spree_core 5.4.3 → 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 (231) 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 +2 -1
  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/ransackable_attributes.rb +5 -1
  28. data/app/models/concerns/spree/search_indexable.rb +8 -7
  29. data/app/models/concerns/spree/searchable.rb +11 -2
  30. data/app/models/concerns/spree/stores/channels.rb +20 -0
  31. data/app/models/concerns/spree/stores/markets.rb +21 -5
  32. data/app/models/concerns/spree/typed_associations.rb +120 -0
  33. data/app/models/concerns/spree/user_methods.rb +71 -12
  34. data/app/models/spree/ability.rb +4 -117
  35. data/app/models/spree/api_key.rb +53 -0
  36. data/app/models/spree/asset.rb +28 -5
  37. data/app/models/spree/authentication/strategy_registry.rb +72 -0
  38. data/app/models/spree/base.rb +18 -1
  39. data/app/models/spree/channel.rb +159 -0
  40. data/app/models/spree/country.rb +2 -0
  41. data/app/models/spree/current.rb +5 -1
  42. data/app/models/spree/custom_field.rb +9 -0
  43. data/app/models/spree/custom_field_definition.rb +7 -0
  44. data/app/models/spree/customer_group.rb +8 -2
  45. data/app/models/spree/export.rb +30 -3
  46. data/app/models/spree/gateway.rb +25 -0
  47. data/app/models/spree/gift_card.rb +1 -1
  48. data/app/models/spree/gift_card_batch.rb +4 -1
  49. data/app/models/spree/import.rb +5 -0
  50. data/app/models/spree/import_row.rb +12 -0
  51. data/app/models/spree/line_item.rb +6 -1
  52. data/app/models/spree/market.rb +32 -1
  53. data/app/models/spree/metafield.rb +38 -0
  54. data/app/models/spree/metafield_definition.rb +29 -6
  55. data/app/models/spree/metafields/json.rb +10 -0
  56. data/app/models/spree/newsletter_subscriber.rb +19 -3
  57. data/app/models/spree/option_type.rb +48 -7
  58. data/app/models/spree/order/checkout.rb +3 -3
  59. data/app/models/spree/order.rb +102 -6
  60. data/app/models/spree/order_approval.rb +19 -0
  61. data/app/models/spree/order_cancellation.rb +19 -0
  62. data/app/models/spree/order_routing/has_strategy_preference.rb +28 -0
  63. data/app/models/spree/order_routing/rules/default_location.rb +16 -0
  64. data/app/models/spree/order_routing/rules/minimize_splits.rb +45 -0
  65. data/app/models/spree/order_routing/rules/preferred_location.rb +22 -0
  66. data/app/models/spree/order_routing/strategy/base.rb +47 -0
  67. data/app/models/spree/order_routing/strategy/legacy.rb +33 -0
  68. data/app/models/spree/order_routing/strategy/reducer.rb +68 -0
  69. data/app/models/spree/order_routing/strategy/rules.rb +81 -0
  70. data/app/models/spree/order_routing_rule.rb +75 -0
  71. data/app/models/spree/permission_sets/configuration_management.rb +16 -0
  72. data/app/models/spree/permission_sets/product_display.rb +2 -0
  73. data/app/models/spree/permission_sets/product_management.rb +2 -0
  74. data/app/models/spree/price.rb +14 -1
  75. data/app/models/spree/price_list.rb +129 -17
  76. data/app/models/spree/price_rule.rb +11 -1
  77. data/app/models/spree/price_rules/customer_group_rule.rb +15 -1
  78. data/app/models/spree/price_rules/market_rule.rb +16 -1
  79. data/app/models/spree/price_rules/user_rule.rb +21 -2
  80. data/app/models/spree/product/channels.rb +149 -0
  81. data/app/models/spree/product/legacy_multi_store_support.rb +40 -0
  82. data/app/models/spree/product/slugs.rb +1 -1
  83. data/app/models/spree/product.rb +172 -31
  84. data/app/models/spree/product_publication.rb +43 -0
  85. data/app/models/spree/promotion/actions/create_adjustment.rb +4 -0
  86. data/app/models/spree/promotion/actions/create_item_adjustments.rb +4 -0
  87. data/app/models/spree/promotion/actions/create_line_items.rb +32 -14
  88. data/app/models/spree/promotion/rules/country.rb +40 -18
  89. data/app/models/spree/promotion/rules/customer_group.rb +10 -1
  90. data/app/models/spree/promotion/rules/product.rb +4 -0
  91. data/app/models/spree/promotion/rules/taxon.rb +24 -1
  92. data/app/models/spree/promotion/rules/user.rb +21 -0
  93. data/app/models/spree/promotion/rules/user_logged_in.rb +6 -0
  94. data/app/models/spree/promotion.rb +22 -1
  95. data/app/models/spree/promotion_action.rb +17 -11
  96. data/app/models/spree/promotion_rule.rb +17 -18
  97. data/app/models/spree/search_provider/meilisearch.rb +12 -2
  98. data/app/models/spree/stock/availability_validator.rb +1 -1
  99. data/app/models/spree/stock/quantifier.rb +89 -9
  100. data/app/models/spree/stock_item.rb +36 -0
  101. data/app/models/spree/stock_location.rb +52 -0
  102. data/app/models/spree/stock_reservation.rb +38 -0
  103. data/app/models/spree/stock_reservations/insufficient_stock_error.rb +12 -0
  104. data/app/models/spree/store.rb +18 -72
  105. data/app/models/spree/store_credit.rb +0 -8
  106. data/app/models/spree/store_product.rb +11 -23
  107. data/app/models/spree/taxon.rb +0 -5
  108. data/app/models/spree/user_identity.rb +1 -2
  109. data/app/models/spree/variant.rb +132 -18
  110. data/app/models/spree/variant_media.rb +46 -0
  111. data/app/models/spree/webhook_delivery.rb +1 -1
  112. data/app/models/spree/webhook_endpoint.rb +24 -0
  113. data/app/models/spree/wished_item.rb +0 -13
  114. data/app/presenters/spree/csv/product_variant_presenter.rb +23 -3
  115. data/app/presenters/spree/search_provider/product_presenter.rb +11 -4
  116. data/app/presenters/spree/variant_presenter.rb +4 -3
  117. data/app/services/spree/addresses/update.rb +6 -8
  118. data/app/services/spree/cart/add_item.rb +10 -0
  119. data/app/services/spree/cart/empty.rb +2 -0
  120. data/app/services/spree/cart/remove_line_item.rb +10 -0
  121. data/app/services/spree/cart/remove_out_of_stock_items.rb +1 -1
  122. data/app/services/spree/cart/set_quantity.rb +10 -0
  123. data/app/services/spree/carts/complete.rb +1 -0
  124. data/app/services/spree/carts/create.rb +1 -0
  125. data/app/services/spree/carts/update.rb +18 -2
  126. data/app/services/spree/carts/upsert_items.rb +6 -6
  127. data/app/services/spree/imports/row_processors/customer.rb +4 -1
  128. data/app/services/spree/imports/row_processors/product_variant.rb +95 -57
  129. data/app/services/spree/newsletter/link_user.rb +53 -0
  130. data/app/services/spree/newsletter/subscribe.rb +31 -9
  131. data/app/services/spree/orders/approve.rb +27 -6
  132. data/app/services/spree/orders/build_shipments.rb +29 -0
  133. data/app/services/spree/orders/cancel.rb +34 -3
  134. data/app/services/spree/orders/complete.rb +53 -0
  135. data/app/services/spree/orders/create.rb +156 -0
  136. data/app/services/spree/orders/update.rb +51 -0
  137. data/app/services/spree/orders/upsert_items.rb +70 -0
  138. data/app/services/spree/prices/bulk_upsert.rb +201 -0
  139. data/app/services/spree/products/duplicator.rb +1 -1
  140. data/app/services/spree/products/prepare_nested_attributes.rb +2 -30
  141. data/app/services/spree/sample_data/loader.rb +30 -0
  142. data/app/services/spree/stock_reservations/extend.rb +19 -0
  143. data/app/services/spree/stock_reservations/release.rb +12 -0
  144. data/app/services/spree/stock_reservations/reserve.rb +103 -0
  145. data/app/services/spree/taxons/remove_products.rb +7 -1
  146. data/app/subscribers/spree/product_metrics_subscriber.rb +3 -7
  147. data/app/views/spree/invitation_mailer/invitation_email.html.erb +4 -0
  148. data/config/locales/en.yml +27 -10
  149. data/config/routes.rb +9 -0
  150. data/db/migrate/20260429000001_create_spree_order_cancellations.rb +25 -0
  151. data/db/migrate/20260429000002_create_spree_order_approvals.rb +22 -0
  152. data/db/migrate/20260429000003_add_status_to_spree_orders.rb +6 -0
  153. data/db/migrate/20260429000004_add_scopes_to_spree_api_keys.rb +11 -0
  154. data/db/migrate/20260501000001_create_spree_stock_reservations.rb +19 -0
  155. data/db/migrate/20260507162651_create_spree_variant_media.rb +23 -0
  156. data/db/migrate/20260508175303_add_pickup_to_spree_stock_locations.rb +12 -0
  157. data/db/migrate/20260508204040_create_spree_channels.rb +18 -0
  158. data/db/migrate/20260508204041_create_spree_order_routing_rules.rb +18 -0
  159. data/db/migrate/20260508204042_add_preferred_stock_location_to_spree_orders.rb +5 -0
  160. data/db/migrate/20260508204043_add_channel_id_to_spree_orders.rb +10 -0
  161. data/db/migrate/20260511000001_backfill_status_on_spree_orders.rb +57 -0
  162. data/db/migrate/20260515000001_add_store_id_to_spree_newsletter_subscribers.rb +25 -0
  163. data/db/migrate/20260529000001_add_unique_index_to_spree_price_rules.rb +41 -0
  164. data/db/migrate/20260529000002_add_unique_index_to_spree_promotion_rules.rb +37 -0
  165. data/db/migrate/20260601000001_create_spree_product_publications.rb +14 -0
  166. data/db/migrate/20260601000002_add_store_id_to_spree_products.rb +16 -0
  167. data/db/migrate/20260602000001_add_default_to_spree_channels.rb +14 -0
  168. data/db/sample_data/channels.rb +12 -0
  169. data/db/sample_data/orders.rb +1 -1
  170. data/db/sample_data/products.csv +212 -212
  171. data/lib/generators/spree/api_resource/api_resource_generator.rb +353 -0
  172. data/lib/generators/spree/api_resource/templates/admin_controller.rb.tt +23 -0
  173. data/lib/generators/spree/api_resource/templates/admin_controller_spec.rb.tt +59 -0
  174. data/lib/generators/spree/api_resource/templates/admin_serializer.rb.tt +11 -0
  175. data/lib/generators/spree/api_resource/templates/factory.rb.tt +26 -0
  176. data/lib/generators/spree/api_resource/templates/store_aliased_serializer.rb.tt +12 -0
  177. data/lib/generators/spree/api_resource/templates/store_controller.rb.tt +31 -0
  178. data/lib/generators/spree/api_resource/templates/store_controller_spec.rb.tt +61 -0
  179. data/lib/generators/spree/api_resource/templates/store_serializer.rb.tt +14 -0
  180. data/lib/generators/spree/controller_decorator/controller_decorator_generator.rb +66 -0
  181. data/lib/generators/spree/controller_decorator/templates/controller_decorator.rb.tt +25 -0
  182. data/lib/generators/spree/model/model_generator.rb +73 -7
  183. data/lib/generators/spree/model/templates/create_table_migration.rb.tt +40 -0
  184. data/lib/generators/spree/model/templates/model.rb.tt +28 -2
  185. data/lib/spree/core/configuration.rb +7 -0
  186. data/lib/spree/core/controller_helpers/auth.rb +0 -12
  187. data/lib/spree/core/controller_helpers/currency.rb +0 -17
  188. data/lib/spree/core/controller_helpers/order.rb +0 -19
  189. data/lib/spree/core/dependencies.rb +5 -2
  190. data/lib/spree/core/engine.rb +54 -7
  191. data/lib/spree/core/permission_configuration.rb +15 -0
  192. data/lib/spree/core/preferences/masking.rb +47 -0
  193. data/lib/spree/core/preferences/preferable_class_methods.rb +7 -1
  194. data/lib/spree/core/version.rb +1 -1
  195. data/lib/spree/core.rb +56 -5
  196. data/lib/spree/permitted_attributes.rb +9 -7
  197. data/lib/spree/testing_support/factories/address_factory.rb +16 -9
  198. data/lib/spree/testing_support/factories/api_key_factory.rb +1 -0
  199. data/lib/spree/testing_support/factories/channel_factory.rb +8 -0
  200. data/lib/spree/testing_support/factories/line_item_factory.rb +2 -8
  201. data/lib/spree/testing_support/factories/newsletter_subscriber_factory.rb +2 -0
  202. data/lib/spree/testing_support/factories/product_factory.rb +16 -7
  203. data/lib/spree/testing_support/factories/product_publication_factory.rb +6 -0
  204. data/lib/spree/testing_support/factories/refresh_token_factory.rb +15 -0
  205. data/lib/spree/testing_support/factories/stock_location_factory.rb +2 -2
  206. data/lib/spree/testing_support/factories/stock_reservation_factory.rb +31 -0
  207. data/lib/spree/testing_support/factories/variant_factory.rb +3 -3
  208. data/lib/spree/testing_support/order_walkthrough.rb +1 -1
  209. data/lib/spree/testing_support/store.rb +10 -0
  210. data/lib/spree/upgrades/5_4_to_5_5/manifest.yml +53 -0
  211. data/lib/tasks/channels.rake +94 -0
  212. data/lib/tasks/core.rake +1 -0
  213. data/lib/tasks/media.rake +27 -0
  214. data/lib/tasks/products.rake +4 -6
  215. data/lib/tasks/publications.rake +60 -0
  216. data/lib/tasks/upgrade.rake +211 -0
  217. metadata +83 -18
  218. data/app/finders/spree/variants/visible_finder.rb +0 -23
  219. data/app/paginators/spree/shared/paginate.rb +0 -30
  220. data/app/presenters/spree/filters/price_presenter.rb +0 -23
  221. data/app/presenters/spree/filters/price_range_presenter.rb +0 -30
  222. data/app/presenters/spree/filters/quantified_price_range_presenter.rb +0 -45
  223. data/app/presenters/spree/product_summary_presenter.rb +0 -27
  224. data/app/presenters/spree/variants/options_presenter.rb +0 -82
  225. data/app/services/spree/classifications/reposition.rb +0 -23
  226. data/app/sorters/spree/orders/sort.rb +0 -10
  227. data/lib/spree/core/controller_helpers/common.rb +0 -14
  228. data/lib/spree/core/token_generator.rb +0 -23
  229. data/lib/spree/database_type_utilities.rb +0 -22
  230. data/lib/spree/testing_support/bar_ability.rb +0 -14
  231. data/lib/spree/testing_support/factories/store_product_factory.rb +0 -6
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: d8ff8a33cfaa4b9554bb3658995a2ed99d1715cb2365d3da012fce5dd2bbd518
4
- data.tar.gz: fd19b449aa211be4cbec760db28e573064f9112d8c0922bf78517e51c0fc5002
3
+ metadata.gz: '017291169758b3fc4f9791b940c3264c91df65cd9197621ca0cafd8e3ade2ba1'
4
+ data.tar.gz: a4f6cfd15c25ce054ce792d545eaf6db7e815b023d66348b4c44d5a226abb3c2
5
5
  SHA512:
6
- metadata.gz: 5a392b8f08c1732f3b661a8758d37d63ac70e2aa7cf39d01dff5521c0e3267b1ee87ed190ea2cdddf914c7bf83462ebb6c7809736b5243610ab6d5e55c411300
7
- data.tar.gz: 841e2707d2535a628a1cc2c743d594276a1cda0a969dd243ed8d82329f53f5d96bda3433517e40d0e261ef79159a7faae9cb57ebb1c5975b6bd2ea00f17d7641
6
+ metadata.gz: 83d3c2a6c3b13d4c508ab4f4c5c15deb544eb3ba7da48ddd966d872d76ec301f7b87093bc0eb42bd12fe1507575ca09b82919796b079a4aa4e66d71d76dd6332
7
+ data.tar.gz: 4453580426836b0b3333309c1eaadabf4bca2fa1fbd8cea51ab1fc1b59c9e02409d098f63e7c77e5dcce222746263d53e021ec3f1b724fd2daa71c623ca573d0
@@ -22,82 +22,10 @@ module Spree
22
22
  end.sort_by { |c| c.name.parameterize }
23
23
  end
24
24
 
25
- def spree_resource_path(resource)
26
- Spree::Deprecation.warn('BaseHelper#spree_resource_path is deprecated and will be removed in Spree 5.5')
27
-
28
- last_word = resource.class.name.split('::', 10).last
29
-
30
- spree_class_name_as_path(last_word)
31
- end
32
-
33
- def spree_class_name_as_path(class_name)
34
- Spree::Deprecation.warn('BaseHelper#spree_class_name_as_path is deprecated and will be removed in Spree 5.5')
35
-
36
- class_name.underscore.humanize.parameterize(separator: '_')
37
- end
38
-
39
- def display_price(product_or_variant)
40
- Spree::Deprecation.warn('display_price is deprecated and will be removed in Spree 5.5. Use variant.price_for(context).display_amount instead.')
41
-
42
- product_or_variant.
43
- price_in(current_currency).
44
- display_price_including_vat_for(current_price_options).
45
- to_html
46
- end
47
-
48
- def display_compare_at_price(product_or_variant)
49
- Spree::Deprecation.warn('display_compare_at_price is deprecated and will be removed in Spree 5.5. Use variant.price_for(context).display_compare_at_amount instead.')
50
-
51
- product_or_variant.
52
- price_in(current_currency).
53
- display_compare_at_price_including_vat_for(current_price_options).
54
- to_html
55
- end
56
-
57
- def link_to_tracking(shipment, options = {})
58
- Spree::Deprecation.warn('BaseHelper#link_to_tracking is deprecated and will be removed in Spree 5.5. Please use shipment.tracking_url instead.')
59
-
60
- return unless shipment.tracking && shipment.shipping_method
61
-
62
- options[:target] ||= :blank
63
-
64
- if shipment.tracking_url
65
- link_to(shipment.tracking, shipment.tracking_url, options)
66
- else
67
- content_tag(:span, shipment.tracking)
68
- end
69
- end
70
-
71
25
  def object
72
26
  instance_variable_get('@' + controller_name.singularize)
73
27
  end
74
28
 
75
- def pretty_time(time)
76
- return '' if time.blank?
77
-
78
- Spree::Deprecation.warn('BaseHelper#pretty_time is deprecated and will be removed in Spree 5.5. Please add `local_time` gem to your Gemfile and use `local_time(time)` instead')
79
-
80
- [I18n.l(time.to_date, format: :long), time.strftime('%l:%M %p %Z')].join(' ')
81
- end
82
-
83
- def pretty_date(date)
84
- return '' if date.blank?
85
-
86
- Spree::Deprecation.warn('BaseHelper#pretty_date is deprecated and will be removed in Spree 5.5. Please add `local_time` gem to your Gemfile and use `local_date(date)` instead')
87
-
88
- [I18n.l(date.to_date, format: :long)].join(' ')
89
- end
90
-
91
- def seo_url(taxon, options = {})
92
- Spree::Deprecation.warn('BaseHelper#seo_url is deprecated and will be removed in Spree 5.5. Please use spree_storefront_resource_url')
93
- spree.nested_taxons_path(taxon.permalink, options.merge(locale: locale_param))
94
- end
95
-
96
- def frontend_available?
97
- Spree::Deprecation.warn('BaseHelper#frontend_available? is deprecated and will be removed in Spree 5.5')
98
- Spree::Core::Engine.frontend_available?
99
- end
100
-
101
29
  # returns the URL of an object on the storefront
102
30
  # @param resource [Spree::Product, Spree::Taxon, Spree::Page] the resource to get the URL for
103
31
  # @param options [Hash] the options for the URL
@@ -160,11 +88,6 @@ module Spree
160
88
  product_or_variant.primary_media
161
89
  end
162
90
 
163
- def base_cache_key
164
- Spree::Deprecation.warn('`base_cache_key` is deprecated and will be removed in Spree 5.5. Please use `spree_base_cache_key` instead')
165
- spree_base_cache_key
166
- end
167
-
168
91
  def spree_base_cache_key
169
92
  @spree_base_cache_key ||= [
170
93
  I18n.locale,
@@ -178,11 +101,6 @@ module Spree
178
101
  ->(record = nil) { [*spree_base_cache_key, record].compact_blank }
179
102
  end
180
103
 
181
- def maximum_quantity
182
- Spree::Deprecation.warn('BaseHelper#maximum_quantity is deprecated and will be removed in Spree 5.5')
183
- Spree::DatabaseTypeUtilities::INTEGER_MAX
184
- end
185
-
186
104
  def payment_method_icon_tag(payment_method, opts = {})
187
105
  return '' unless defined?(inline_svg)
188
106
 
@@ -42,18 +42,6 @@ module Spree
42
42
  [label, currency]
43
43
  end
44
44
 
45
- # Returns the list of supported currencies for the current store.
46
- # @return [Array<Money::Currency>] the list of supported currencies
47
- def preferred_currencies
48
- Spree::Deprecation.warn('preferred_currencies is deprecated and will be removed in Spree 5.5. Use current_store.supported_currencies_list instead.')
49
- @preferred_currencies ||= current_store.supported_currencies_list
50
- end
51
-
52
- def preferred_currencies_select_options
53
- Spree::Deprecation.warn('preferred_currencies_select_options is deprecated and will be removed in Spree 5.5. Use supported_currency_options instead.')
54
- preferred_currencies.map { |currency| currency_presentation(currency) }
55
- end
56
-
57
45
  def currency_money(currency = current_currency)
58
46
  ::Money::Currency.find(currency)
59
47
  end
@@ -120,14 +120,6 @@ module Spree
120
120
  limit(Spree::Config[:products_per_page])
121
121
  end
122
122
 
123
- def related_products
124
- Spree::Deprecation.warn('ProductsHelper#related_products is deprecated and will be removed in Spree 5.5. Please use ProductsHelper#relations from now on.')
125
-
126
- return [] unless @product.respond_to?(:has_related_products?)
127
-
128
- @related_products ||= relations_by_type('related_products')
129
- end
130
-
131
123
  def product_available_in_currency?
132
124
  !(@product_price.nil? || @product_price.zero?)
133
125
  end
@@ -1,5 +1,23 @@
1
1
  module Spree
2
+ # Shared base for every Spree job.
3
+ #
4
+ # Retries only transient infrastructure errors. Broad replay is unsafe here because
5
+ # most jobs have non-idempotent post-work side effects (counters, state transitions,
6
+ # lifecycle events, external calls); jobs whose work is retry-safe opt in to
7
+ # `retry_on StandardError` themselves (see `Spree::WebhookDeliveryJob`,
8
+ # `Spree::Events::SubscriberJob`). RecordNotFound gets its own tighter policy
9
+ # to absorb the Sidekiq enqueue-vs-DB-commit race (sub-second window) without
10
+ # holding the queue for genuine deletes.
2
11
  class BaseJob < ApplicationJob
3
12
  queue_as Spree.queues.default
13
+
14
+ retry_on ActiveRecord::Deadlocked,
15
+ ActiveRecord::LockWaitTimeout,
16
+ ActiveRecord::ConnectionNotEstablished,
17
+ ActiveRecord::ConnectionFailed,
18
+ wait: :polynomially_longer, attempts: 5
19
+ retry_on ActiveRecord::RecordNotFound, wait: 2.seconds, attempts: 3
20
+
21
+ discard_on ActiveJob::DeserializationError
4
22
  end
5
23
  end
@@ -16,7 +16,8 @@ module Spree
16
16
  class SubscriberJob < Spree::BaseJob
17
17
  queue_as Spree.queues.events
18
18
 
19
- # Retry configuration
19
+ # Subscribers run user-defined code that can hit external services; broad retry
20
+ # is intentional.
20
21
  retry_on StandardError, wait: :polynomially_longer, attempts: 3
21
22
 
22
23
  discard_on ActiveJob::DeserializationError do |job, error|
@@ -3,6 +3,17 @@ module Spree
3
3
  class GenerateJob < Spree::BaseJob
4
4
  queue_as Spree.queues.exports
5
5
 
6
+ # `Export#generate` is not retry-safe: each call re-attaches a new ActiveStorage
7
+ # blob (leaving the prior one orphaned) and re-enqueues the completion email.
8
+ # Opt out of the parent's retry policy so transient errors fail fast into the
9
+ # dead queue for operator review rather than producing duplicate side effects.
10
+ retry_on ActiveRecord::Deadlocked,
11
+ ActiveRecord::LockWaitTimeout,
12
+ ActiveRecord::ConnectionNotEstablished,
13
+ ActiveRecord::ConnectionFailed,
14
+ ActiveRecord::RecordNotFound,
15
+ attempts: 1
16
+
6
17
  def perform(export_id)
7
18
  export = Spree::Export.find_by_prefix_id!(export_id)
8
19
  export.generate
@@ -11,7 +11,7 @@ module Spree
11
11
  discard_on URI::InvalidURIError
12
12
  discard_on SsrfFilter::Error
13
13
 
14
- def perform(viewable_id, viewable_type, external_url, external_id = nil, position = nil)
14
+ def perform(viewable_id, viewable_type, external_url, external_id = nil, position = nil, link_variant_id = nil)
15
15
  viewable = viewable_type.safe_constantize.find(viewable_id)
16
16
 
17
17
  Spree::Image.ensure_metafield_definition_exists!(Spree::Image::EXTERNAL_URL_METAFIELD_KEY)
@@ -30,9 +30,14 @@ module Spree
30
30
 
31
31
  # don't re-download the image if it's already been downloaded
32
32
  # still trigger save! if position has changed
33
- image.save! and return if image_already_saved?(image, external_url)
33
+ if image_already_saved?(image, external_url)
34
+ image.save!
35
+ link_to_variant(image, link_variant_id)
36
+ return
37
+ end
34
38
 
35
39
  download_and_attach_image(external_url, image, external_id)
40
+ link_to_variant(image, link_variant_id)
36
41
  rescue ActiveStorage::IntegrityError => e
37
42
  raise e unless Rails.env.test?
38
43
  end
@@ -85,21 +90,31 @@ module Spree
85
90
  image.persisted? && image.attachment.attached? && image.external_url.present? && external_url == image.external_url
86
91
  end
87
92
 
93
+ # `Product#images` delegates to the master variant (legacy alias) — use
94
+ # `Product#media` so 5.5 product-level uploads don't get re-pinned to master.
95
+ def viewable_assets(viewable)
96
+ viewable.is_a?(Spree::Product) ? viewable.media : viewable.images
97
+ end
98
+
88
99
  def image_scope(viewable)
89
- if Spree::Image.respond_to?(:with_deleted)
90
- viewable.images.with_deleted
91
- else
92
- viewable.images
93
- end
100
+ scope = viewable_assets(viewable)
101
+ scope.respond_to?(:with_deleted) ? scope.with_deleted : scope
94
102
  end
95
103
 
96
104
  def find_or_initialize_image(viewable, external_url, external_id = nil)
97
105
  if external_id.present? && viewable.respond_to?(:external_id)
98
106
  image_scope(viewable).find_or_initialize_by(external_id: external_id)
99
107
  else
100
- image_scope(viewable).with_external_url(external_url).first || viewable.images.new
108
+ image_scope(viewable).with_external_url(external_url).first || viewable_assets(viewable).new
101
109
  end
102
110
  end
111
+
112
+ def link_to_variant(image, variant_id)
113
+ return if variant_id.blank?
114
+ return unless image.persisted? && image.viewable_type == 'Spree::Product'
115
+
116
+ Spree::VariantMedia.find_or_create_by(variant_id: variant_id, media_id: image.id)
117
+ end
103
118
  end
104
119
  end
105
120
  end
@@ -0,0 +1,11 @@
1
+ module Spree
2
+ module Imports
3
+ class AssignTagsJob < Spree::Imports::BaseJob
4
+ def perform(product_id, tags)
5
+ product = Spree::Product.find(product_id)
6
+ product.tag_list = tags
7
+ product.save!
8
+ end
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,15 @@
1
+ module Spree
2
+ module Imports
3
+ # Shared base for every job in the imports pipeline.
4
+ #
5
+ # The narrow transient-error retry policy is inherited from `Spree::BaseJob`;
6
+ # we only override the queue here. Per-row business errors are caught inside
7
+ # `Spree::ImportRow#process!` and converted to `row.fail!`, so they never
8
+ # bubble up to the job layer. Subclasses may extend the retry list (e.g.
9
+ # `CreateCategoriesJob` adds `RecordNotUnique` to recover from concurrent
10
+ # taxon creation races).
11
+ class BaseJob < Spree::BaseJob
12
+ queue_as Spree.queues.imports
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,37 @@
1
+ module Spree
2
+ module Imports
3
+ class CreateCategoriesJob < Spree::Imports::BaseJob
4
+ # Concurrent imports can race on `with_matching_name(...).first || create!(...)`
5
+ # for the same taxonomy/taxon name and hit the unique index; a retry then finds
6
+ # the peer's committed row.
7
+ retry_on ActiveRecord::RecordNotUnique, wait: :polynomially_longer, attempts: 5
8
+
9
+ def perform(product_id, store_id, taxon_pretty_names)
10
+ product = Spree::Product.find(product_id)
11
+ store = Spree::Store.find(store_id)
12
+ taxons = taxon_pretty_names.filter_map { |taxon_pretty_name| find_or_create_taxon(store, taxon_pretty_name) }
13
+
14
+ product.taxons = taxons
15
+ end
16
+
17
+ private
18
+
19
+ def find_or_create_taxon(store, taxon_pretty_name)
20
+ taxon_names = taxon_pretty_name.strip.split('->').map(&:strip).map(&:presence).compact
21
+ return if taxon_names.empty?
22
+
23
+ taxonomy_name = taxon_names.shift
24
+ taxonomy = store.taxonomies.with_matching_name(taxonomy_name).first || store.taxonomies.create!(name: taxonomy_name)
25
+
26
+ last_taxon = taxonomy.root
27
+
28
+ taxon_names.each do |taxon_name|
29
+ last_taxon = taxonomy.taxons.with_matching_name(taxon_name).where(parent: last_taxon).first ||
30
+ taxonomy.taxons.create!(name: taxon_name, parent: last_taxon)
31
+ end
32
+
33
+ last_taxon
34
+ end
35
+ end
36
+ end
37
+ end
@@ -1,8 +1,6 @@
1
1
  module Spree
2
2
  module Imports
3
- class CreateRowsJob < Spree::BaseJob
4
- queue_as Spree.queues.imports
5
-
3
+ class CreateRowsJob < Spree::Imports::BaseJob
6
4
  discard_on ::CSV::MalformedCSVError, EncodingError do |job, error|
7
5
  import = Spree::Import.find(job.arguments.first)
8
6
  import.update_columns(processing_errors: error.message, status: :failed, updated_at: Time.current)
@@ -1,8 +1,6 @@
1
1
  module Spree
2
2
  module Imports
3
- class ProcessGroupJob < Spree::BaseJob
4
- queue_as Spree.queues.imports
5
-
3
+ class ProcessGroupJob < Spree::Imports::BaseJob
6
4
  def perform(import_id, row_ids)
7
5
  import = Spree::Import.find(import_id)
8
6
  Spree::Current.store = import.store
@@ -10,7 +8,8 @@ module Spree
10
8
  mappings = import.mappings.mapped.to_a
11
9
  schema_fields = import.schema_fields
12
10
  large = import.large_import?
13
- rows = import.rows.where(id: row_ids).order(:row_number)
11
+ # Skip rows already completed on a prior attempt so retries don't double-process them.
12
+ rows = import.rows.where(id: row_ids).pending_and_failed.order(:row_number)
14
13
 
15
14
  if large
16
15
  Spree::Events.disable do
@@ -27,14 +26,17 @@ module Spree
27
26
 
28
27
  private
29
28
 
29
+ # Completion is row-state-derived so retry-induced over-increments of the counter
30
+ # stay harmless. The counter pre-check just shortcuts the row scan for workers
31
+ # that obviously can't be the last group to finish. `in_flight` excludes orphaned
32
+ # `processing` rows past the stall window so a dead worker can't block completion.
30
33
  def check_import_completion(import, large)
31
34
  Spree::Import.where(id: import.id).update_all(
32
35
  'completed_groups_count = COALESCE(completed_groups_count, 0) + 1'
33
36
  )
34
37
  import.reload
35
38
 
36
- if import.completed_groups_count >= import.processing_groups_count
37
- # Guard against concurrent workers both reaching this point
39
+ if import.completed_groups_count >= import.processing_groups_count && import.rows.in_flight.none?
38
40
  import.complete! if import.status == 'processing'
39
41
  elsif large && (import.completed_groups_count % 10).zero?
40
42
  import.publish_event('import.progress')
@@ -1,8 +1,6 @@
1
1
  module Spree
2
2
  module Imports
3
- class ProcessRowsJob < Spree::BaseJob
4
- queue_as Spree.queues.imports
5
-
3
+ class ProcessRowsJob < Spree::Imports::BaseJob
6
4
  BATCH_SIZE = 100
7
5
 
8
6
  def perform(import_id)
@@ -0,0 +1,83 @@
1
+ module Spree
2
+ module Media
3
+ # Per-product worker for the 5.4 → 5.5 media migration. Idempotent.
4
+ class MigrateProductAssetsJob < Spree::BaseJob
5
+ queue_as Spree.queues.images
6
+
7
+ def perform(product_id)
8
+ product = Spree::Product.includes(:master, :variants).find_by(id: product_id)
9
+ return unless product
10
+
11
+ # One query for all variant-pinned assets on the product (master +
12
+ # non-master). Grouping by viewable_id avoids N queries per variant.
13
+ viewable_ids = product.variants.map(&:id)
14
+ viewable_ids << product.master.id if product.master
15
+ return if viewable_ids.empty?
16
+
17
+ assets_by_variant = Spree::Asset
18
+ .where(viewable_type: 'Spree::Variant', viewable_id: viewable_ids)
19
+ .pluck(:id, :viewable_id)
20
+ .group_by(&:last)
21
+ .transform_values { |rows| rows.map(&:first) }
22
+ return if assets_by_variant.empty?
23
+
24
+ master_id = product.master&.id
25
+ touched_variants = []
26
+
27
+ product.variants.each do |variant|
28
+ asset_ids = assets_by_variant[variant.id]
29
+ next if asset_ids.blank?
30
+
31
+ move_assets_to_product(asset_ids, product)
32
+ link_assets_to_variant(asset_ids, variant.id)
33
+ touched_variants << variant
34
+ end
35
+
36
+ if master_id && (master_asset_ids = assets_by_variant[master_id]).present?
37
+ move_assets_to_product(master_asset_ids, product)
38
+ touched_variants << product.master
39
+ end
40
+
41
+ # update_all + upsert_all skip callbacks, so refresh thumbnails by hand.
42
+ touched_variants.each(&:update_thumbnail!)
43
+ recalculate_counters(product)
44
+ end
45
+
46
+ private
47
+
48
+ def move_assets_to_product(asset_ids, product)
49
+ Spree::Asset.where(id: asset_ids).update_all(
50
+ viewable_type: 'Spree::Product',
51
+ viewable_id: product.id,
52
+ updated_at: Time.current
53
+ )
54
+ end
55
+
56
+ def link_assets_to_variant(asset_ids, variant_id)
57
+ rows = asset_ids.map { |asset_id| { variant_id: variant_id, media_id: asset_id } }
58
+
59
+ # MySQL infers the conflict target from the unique index; only PG/SQLite
60
+ # need the explicit `unique_by:`.
61
+ opts = { on_duplicate: :skip }
62
+ if %w[PostgreSQL SQLite].include?(ActiveRecord::Base.connection.adapter_name)
63
+ opts[:unique_by] = :idx_variant_media_unique
64
+ end
65
+
66
+ Spree::VariantMedia.upsert_all(rows, **opts)
67
+ end
68
+
69
+ def recalculate_counters(product)
70
+ new_count = product.media.count
71
+ new_primary_id = product.media.order(:position).pick(:id)
72
+
73
+ return if product.media_count == new_count && product.primary_media_id == new_primary_id
74
+
75
+ product.update_columns(
76
+ media_count: new_count,
77
+ primary_media_id: new_primary_id,
78
+ updated_at: Time.current
79
+ )
80
+ end
81
+ end
82
+ end
83
+ end
@@ -3,11 +3,22 @@ module Spree
3
3
  class RefreshMetricsJob < Spree::BaseJob
4
4
  queue_as Spree.queues.products
5
5
 
6
- def perform(product_id, store_id)
7
- store_product = Spree::StoreProduct.find_by(product_id: product_id, store_id: store_id)
8
- return unless store_product
6
+ def perform(product_id)
7
+ product = Spree::Product.find_by(id: product_id)
8
+ return unless product
9
9
 
10
- store_product.refresh_metrics!
10
+ completed_order_ids = product.completed_orders.select(:id)
11
+ variant_ids = product.variants_including_master.ids
12
+
13
+ line_items = Spree::LineItem.joins(:order)
14
+ .where(spree_orders: { id: completed_order_ids })
15
+ .where(variant_id: variant_ids)
16
+
17
+ # update columns to skip callbacks
18
+ product.update_columns(
19
+ units_sold_count: line_items.sum(:quantity),
20
+ revenue: line_items.sum(:pre_tax_amount)
21
+ )
11
22
  end
12
23
  end
13
24
  end
@@ -3,6 +3,17 @@ module Spree
3
3
  class GenerateJob < Spree::BaseJob
4
4
  queue_as Spree.queues.reports
5
5
 
6
+ # `Report#generate` is not retry-safe: each call re-attaches a new ActiveStorage
7
+ # blob (leaving the prior one orphaned) and re-enqueues the completion email.
8
+ # Opt out of the parent's retry policy so transient errors fail fast into the
9
+ # dead queue for operator review rather than producing duplicate side effects.
10
+ retry_on ActiveRecord::Deadlocked,
11
+ ActiveRecord::LockWaitTimeout,
12
+ ActiveRecord::ConnectionNotEstablished,
13
+ ActiveRecord::ConnectionFailed,
14
+ ActiveRecord::RecordNotFound,
15
+ attempts: 1
16
+
6
17
  def perform(report_id)
7
18
  report = Spree::Report.find_by_prefix_id!(report_id)
8
19
  report.generate
@@ -3,8 +3,12 @@ module Spree
3
3
  class IndexJob < Spree::BaseJob
4
4
  queue_as Spree.queues.search
5
5
 
6
+ # Search providers are external services (Meilisearch, etc.); a transient 5xx or
7
+ # network blip should not drop the index update.
6
8
  retry_on StandardError, wait: :polynomially_longer, attempts: 5
7
- discard_on ActiveRecord::RecordNotFound
9
+ # Must come after `retry_on StandardError` so DeserializationError lands in discard
10
+ # (ActiveJob handler lookup is reverse-declaration-order).
11
+ discard_on ActiveJob::DeserializationError
8
12
 
9
13
  # @param resource_class [String] e.g. 'Spree::Product'
10
14
  # @param resource_id [String] always pass as string for UUID support
@@ -3,7 +3,11 @@ module Spree
3
3
  class RemoveJob < Spree::BaseJob
4
4
  queue_as Spree.queues.search
5
5
 
6
+ # Search providers are external services; broad retry covers network blips and 5xx.
6
7
  retry_on StandardError, wait: :polynomially_longer, attempts: 5
8
+ # Must come after `retry_on StandardError` so DeserializationError lands in discard
9
+ # (ActiveJob handler lookup is reverse-declaration-order).
10
+ discard_on ActiveJob::DeserializationError
7
11
 
8
12
  # @param prefixed_id [String] prefixed ID of the document to remove (e.g. 'prod_abc')
9
13
  # @param store_id [String] always pass as string for UUID support
@@ -0,0 +1,11 @@
1
+ module Spree
2
+ module StockReservations
3
+ class ExpireJob < Spree::BaseJob
4
+ queue_as Spree.queues.stock_reservations
5
+
6
+ def perform
7
+ Spree::StockReservation.expired.in_batches(of: 1_000).delete_all
8
+ end
9
+ end
10
+ end
11
+ end
@@ -18,11 +18,44 @@ module Spree
18
18
  calculator.class.to_s if calculator
19
19
  end
20
20
 
21
+ # Accepts a fully-qualified class name (`'Spree::Calculator::FlatRate'`)
22
+ # or the public API shorthand (`'flat_rate'`). Shorthand is resolved
23
+ # against this parent's registered calculators so a CreateAdjustment
24
+ # can't be assigned a shipping-only calculator just by knowing its
25
+ # name.
21
26
  def calculator_type=(calculator_type)
22
- klass = calculator_type.constantize if calculator_type
27
+ return if calculator_type.blank?
28
+
29
+ str = calculator_type.to_s
30
+ klass =
31
+ if str.include?('::')
32
+ str.safe_constantize
33
+ else
34
+ registry = self.class.respond_to?(:calculators) ? self.class.calculators : []
35
+ registry.find { |k| k.api_type == str }
36
+ end
23
37
  self.calculator = klass.new if klass && !calculator.instance_of?(klass)
24
38
  end
25
39
 
40
+ # API v3 writer for the flat `calculator: { type:, preferences: {} }`
41
+ # payload. Routes preferences through `set_preference` so values are
42
+ # coerced by the typed `preferred_<name>=` setters — direct
43
+ # assignment to the serialized hash would skip coercion.
44
+ def assign_calculator_attributes(attrs)
45
+ return if attrs.nil?
46
+
47
+ attrs = attrs.to_h.with_indifferent_access
48
+ self.calculator_type = attrs[:type] if attrs[:type].present?
49
+
50
+ return if calculator.nil? || attrs[:preferences].blank?
51
+
52
+ attrs[:preferences].to_h.each do |key, value|
53
+ next unless calculator.has_preference?(key.to_sym)
54
+
55
+ calculator.set_preference(key.to_sym, value)
56
+ end
57
+ end
58
+
26
59
  private
27
60
 
28
61
  def self.model_name_without_spree_namespace
@@ -9,11 +9,42 @@ module Spree
9
9
  scope :available_on_front_end, -> { where(display_on: [:front_end, :both]) }
10
10
  scope :available_on_back_end, -> { where(display_on: [:back_end, :both]) }
11
11
 
12
+ # 5.5 → 6.0 bridge: see docs/plans/5.5-6.0-display-on-to-boolean.md.
13
+ # The tri-state `display_on` enum collapses to a single
14
+ # `storefront_visible` boolean in 6.0 — `back_end` becomes `false`,
15
+ # everything else becomes `true`. The legacy `front_end`-only value
16
+ # (visible to customers but hidden from staff) has no real use case
17
+ # and folds into `storefront_visible: true` on migration.
18
+ scope :storefront_visible, -> { where.not(display_on: 'back_end') }
19
+ scope :admin_only, -> { where(display_on: 'back_end') }
20
+
21
+ # Expose `storefront_visible` to Ransack so admin clients can filter
22
+ # by it (e.g. `q[storefront_visible_eq]=true`) without knowing about
23
+ # the underlying tri-state `display_on` column.
24
+ ransacker :storefront_visible, type: :boolean do |parent|
25
+ # Wrap in `Grouping` so Postgres sees `(display_on != 'back_end') = TRUE`
26
+ # instead of the ambiguous `display_on != 'back_end' = TRUE`.
27
+ Arel::Nodes::Grouping.new(
28
+ Arel::Nodes::NotEqual.new(parent.table[:display_on], Arel::Nodes::Quoted.new('back_end'))
29
+ )
30
+ end
31
+
32
+ self.whitelisted_ransackable_attributes =
33
+ (whitelisted_ransackable_attributes || []) | %w[storefront_visible]
34
+
12
35
  validates :display_on, presence: true, inclusion: { in: DISPLAY.map(&:to_s) }
13
36
 
14
37
  def available_on_front_end?
15
38
  display_on == 'front_end' || display_on == 'both'
16
39
  end
40
+
41
+ def storefront_visible
42
+ display_on != 'back_end'
43
+ end
44
+
45
+ def storefront_visible=(value)
46
+ self.display_on = ActiveModel::Type::Boolean.new.cast(value) ? 'both' : 'back_end'
47
+ end
17
48
  end
18
49
  end
19
50
  end