spree_core 4.10.1 → 5.0.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 (476) hide show
  1. checksums.yaml +4 -4
  2. data/app/assets/config/spree_core_manifest.js +4 -0
  3. data/app/assets/images/google_on_white_hdpi.png +0 -0
  4. data/app/controllers/spree/base_controller.rb +1 -4
  5. data/app/finders/spree/orders/find_complete.rb +14 -2
  6. data/app/finders/spree/products/find.rb +74 -23
  7. data/app/finders/spree/stores/find_current.rb +6 -2
  8. data/app/helpers/spree/addresses_helper.rb +106 -0
  9. data/app/helpers/spree/base_helper.rb +83 -55
  10. data/app/helpers/spree/currency_helper.rb +12 -0
  11. data/app/helpers/spree/images_helper.rb +18 -0
  12. data/app/helpers/spree/mail_helper.rb +24 -0
  13. data/app/helpers/spree/products_helper.rb +1 -20
  14. data/app/javascript/spree/core/controllers/address_autocomplete_controller.js +233 -0
  15. data/app/javascript/spree/core/controllers/address_form_controller.js +98 -0
  16. data/app/javascript/spree/core/controllers/enable_button_controller.js +29 -0
  17. data/app/javascript/spree/core/helpers/address_autocomplete/google_places_suggestions_provider.js +179 -0
  18. data/app/javascript/spree/core/helpers/debounce.js +9 -0
  19. data/app/javascript/spree/core/helpers/scroll_lock.js +25 -0
  20. data/app/jobs/spree/addresses/geocode_address_job.rb +31 -0
  21. data/app/jobs/spree/base_job.rb +1 -0
  22. data/app/jobs/spree/coupon_codes/bulk_generate_job.rb +17 -0
  23. data/app/jobs/spree/exports/generate_job.rb +12 -0
  24. data/app/jobs/spree/products/auto_match_taxons_job.rb +14 -0
  25. data/app/jobs/spree/products/touch_taxons_job.rb +12 -0
  26. data/app/jobs/spree/reports/generate_job.rb +12 -0
  27. data/app/jobs/spree/stock_locations/stock_items/create_job.rb +1 -1
  28. data/app/jobs/spree/themes/duplicate_components_job.rb +59 -0
  29. data/app/jobs/spree/themes/screenshot_job.rb +81 -0
  30. data/app/jobs/spree/variants/remove_from_incomplete_orders_job.rb +2 -0
  31. data/app/jobs/spree/variants/remove_line_item_job.rb +2 -0
  32. data/app/mailers/spree/base_mailer.rb +51 -0
  33. data/app/mailers/spree/export_mailer.rb +19 -0
  34. data/app/mailers/spree/report_mailer.rb +20 -0
  35. data/app/mailers/spree/test_mailer.rb +8 -0
  36. data/app/models/action_text/rich_text_decorator.rb +11 -0
  37. data/app/models/action_text/video_embed.rb +13 -0
  38. data/app/models/acts_as_taggable_on/tag_decorator.rb +10 -0
  39. data/app/models/concerns/spree/calculated_adjustments.rb +2 -0
  40. data/app/models/concerns/spree/display_on.rb +13 -0
  41. data/app/models/concerns/spree/has_one_link.rb +42 -0
  42. data/app/models/concerns/spree/has_page_links.rb +53 -0
  43. data/app/models/concerns/spree/metadata.rb +3 -7
  44. data/app/models/concerns/spree/multi_searchable.rb +21 -0
  45. data/app/models/concerns/spree/multi_store_resource.rb +9 -0
  46. data/app/models/concerns/spree/number_as_param.rb +4 -2
  47. data/app/models/concerns/spree/parameterizable_name.rb +21 -0
  48. data/app/models/concerns/spree/previewable.rb +17 -0
  49. data/app/models/concerns/spree/product_scopes.rb +7 -7
  50. data/app/models/concerns/spree/stores/setup.rb +52 -0
  51. data/app/models/concerns/spree/stores/socials.rb +68 -0
  52. data/app/models/concerns/spree/user_address.rb +4 -0
  53. data/app/models/concerns/spree/user_methods.rb +61 -11
  54. data/app/models/concerns/spree/user_reporting.rb +14 -6
  55. data/app/models/concerns/spree/user_roles.rb +1 -1
  56. data/app/models/spree/ability.rb +4 -2
  57. data/app/models/spree/address.rb +122 -28
  58. data/app/models/spree/adjustment.rb +4 -1
  59. data/app/models/spree/asset/support/active_storage.rb +1 -1
  60. data/app/models/spree/asset.rb +18 -6
  61. data/app/models/spree/base.rb +38 -7
  62. data/app/models/spree/base_analytics_event_handler.rb +49 -0
  63. data/app/models/spree/calculator.rb +1 -1
  64. data/app/models/spree/classification.rb +10 -1
  65. data/app/models/spree/country.rb +1 -1
  66. data/app/models/spree/coupon_code.rb +38 -0
  67. data/app/models/spree/credit_card.rb +4 -3
  68. data/app/models/spree/current.rb +9 -0
  69. data/app/models/spree/custom_domain.rb +53 -0
  70. data/app/models/spree/customer_return.rb +13 -1
  71. data/app/models/spree/data_feed.rb +1 -1
  72. data/app/models/spree/digital.rb +5 -6
  73. data/app/models/spree/digital_link.rb +11 -6
  74. data/app/models/spree/export.rb +172 -0
  75. data/app/models/spree/exports/orders.rb +23 -0
  76. data/app/models/spree/exports/products.rb +35 -0
  77. data/app/models/spree/gateway/bogus.rb +5 -0
  78. data/app/models/spree/gateway.rb +14 -4
  79. data/app/models/spree/gateway_customer.rb +13 -0
  80. data/app/models/spree/image/configuration/active_storage.rb +1 -1
  81. data/app/models/spree/inventory_unit.rb +1 -1
  82. data/app/models/spree/legacy_user.rb +3 -1
  83. data/app/models/spree/line_item.rb +37 -2
  84. data/app/models/spree/log_entry.rb +1 -1
  85. data/app/models/spree/option_type.rb +40 -6
  86. data/app/models/spree/option_type_prototype.rb +1 -1
  87. data/app/models/spree/option_value.rb +35 -5
  88. data/app/models/spree/option_value_variant.rb +2 -2
  89. data/app/models/spree/order/address_book.rb +34 -15
  90. data/app/models/spree/order/checkout.rb +12 -2
  91. data/app/models/spree/order/currency_updater.rb +1 -1
  92. data/app/models/spree/order/digital.rb +14 -3
  93. data/app/models/spree/order/emails.rb +1 -1
  94. data/app/models/spree/order/payments.rb +1 -13
  95. data/app/models/spree/order/store_credit.rb +17 -5
  96. data/app/models/spree/order/webhooks.rb +1 -1
  97. data/app/models/spree/order.rb +179 -49
  98. data/app/models/spree/order_promotion.rb +1 -1
  99. data/app/models/spree/order_updater.rb +9 -3
  100. data/app/models/spree/page.rb +184 -0
  101. data/app/models/spree/page_block.rb +69 -0
  102. data/app/models/spree/page_blocks/buttons.rb +29 -0
  103. data/app/models/spree/page_blocks/heading.rb +18 -0
  104. data/app/models/spree/page_blocks/image.rb +17 -0
  105. data/app/models/spree/page_blocks/link.rb +21 -0
  106. data/app/models/spree/page_blocks/mega_nav.rb +33 -0
  107. data/app/models/spree/page_blocks/mega_nav_with_subcategories.rb +32 -0
  108. data/app/models/spree/page_blocks/nav.rb +15 -0
  109. data/app/models/spree/page_blocks/newsletter_form.rb +18 -0
  110. data/app/models/spree/page_blocks/products/buy_buttons.rb +12 -0
  111. data/app/models/spree/page_blocks/products/price.rb +18 -0
  112. data/app/models/spree/page_blocks/products/quantity_selector.rb +12 -0
  113. data/app/models/spree/page_blocks/products/share.rb +8 -0
  114. data/app/models/spree/page_blocks/products/title.rb +15 -0
  115. data/app/models/spree/page_blocks/products/variant_picker.rb +9 -0
  116. data/app/models/spree/page_blocks/subheading.rb +17 -0
  117. data/app/models/spree/page_blocks/text.rb +16 -0
  118. data/app/models/spree/page_link.rb +59 -0
  119. data/app/models/spree/page_section.rb +215 -0
  120. data/app/models/spree/page_sections/announcement_bar.rb +30 -0
  121. data/app/models/spree/page_sections/collection_banner.rb +18 -0
  122. data/app/models/spree/page_sections/custom_code.rb +11 -0
  123. data/app/models/spree/page_sections/featured_posts.rb +43 -0
  124. data/app/models/spree/page_sections/featured_product.rb +46 -0
  125. data/app/models/spree/page_sections/featured_taxon.rb +88 -0
  126. data/app/models/spree/page_sections/featured_taxons.rb +45 -0
  127. data/app/models/spree/page_sections/footer.rb +94 -0
  128. data/app/models/spree/page_sections/header.rb +62 -0
  129. data/app/models/spree/page_sections/image_banner.rb +38 -0
  130. data/app/models/spree/page_sections/image_with_text.rb +48 -0
  131. data/app/models/spree/page_sections/main_password_footer.rb +18 -0
  132. data/app/models/spree/page_sections/main_password_header.rb +20 -0
  133. data/app/models/spree/page_sections/newsletter.rb +47 -0
  134. data/app/models/spree/page_sections/page_title.rb +19 -0
  135. data/app/models/spree/page_sections/post_details.rb +19 -0
  136. data/app/models/spree/page_sections/post_grid.rb +19 -0
  137. data/app/models/spree/page_sections/product_details.rb +16 -0
  138. data/app/models/spree/page_sections/product_grid.rb +13 -0
  139. data/app/models/spree/page_sections/related_products.rb +60 -0
  140. data/app/models/spree/page_sections/rich_text.rb +20 -0
  141. data/app/models/spree/page_sections/taxon_banner.rb +18 -0
  142. data/app/models/spree/page_sections/taxon_grid.rb +17 -0
  143. data/app/models/spree/page_sections/video.rb +99 -0
  144. data/app/models/spree/pages/account.rb +15 -0
  145. data/app/models/spree/pages/cart.rb +9 -0
  146. data/app/models/spree/pages/checkout.rb +9 -0
  147. data/app/models/spree/pages/custom.rb +34 -0
  148. data/app/models/spree/pages/homepage.rb +68 -0
  149. data/app/models/spree/pages/login.rb +15 -0
  150. data/app/models/spree/pages/password.rb +57 -0
  151. data/app/models/spree/pages/post.rb +27 -0
  152. data/app/models/spree/pages/post_list.rb +32 -0
  153. data/app/models/spree/pages/product_details.rb +29 -0
  154. data/app/models/spree/pages/search_results.rb +43 -0
  155. data/app/models/spree/pages/shop_all.rb +36 -0
  156. data/app/models/spree/pages/taxon.rb +29 -0
  157. data/app/models/spree/pages/taxon_list.rb +41 -0
  158. data/app/models/spree/pages/wishlist.rb +11 -0
  159. data/app/models/spree/payment/gateway_options.rb +13 -15
  160. data/app/models/spree/payment/processing.rb +1 -1
  161. data/app/models/spree/payment/webhooks.rb +1 -1
  162. data/app/models/spree/payment.rb +61 -12
  163. data/app/models/spree/payment_capture_event.rb +1 -1
  164. data/app/models/spree/payment_method/store_credit.rb +9 -1
  165. data/app/models/spree/payment_method.rb +26 -8
  166. data/app/models/spree/payment_source.rb +2 -2
  167. data/app/models/spree/post.rb +108 -0
  168. data/app/models/spree/post_category.rb +33 -0
  169. data/app/models/spree/preference.rb +1 -1
  170. data/app/models/spree/price.rb +10 -3
  171. data/app/models/spree/product/webhooks.rb +1 -1
  172. data/app/models/spree/product.rb +325 -54
  173. data/app/models/spree/product_option_type.rb +1 -1
  174. data/app/models/spree/product_promotion_rule.rb +1 -1
  175. data/app/models/spree/product_property.rb +4 -1
  176. data/app/models/spree/promotion/actions/create_line_items.rb +37 -2
  177. data/app/models/spree/promotion/rules/country.rb +22 -4
  178. data/app/models/spree/promotion/rules/currency.rb +21 -0
  179. data/app/models/spree/promotion/rules/item_total.rb +12 -3
  180. data/app/models/spree/promotion/rules/option_value.rb +5 -20
  181. data/app/models/spree/promotion/rules/product.rb +33 -1
  182. data/app/models/spree/promotion/rules/taxon.rb +31 -0
  183. data/app/models/spree/promotion/rules/user.rb +33 -4
  184. data/app/models/spree/promotion.rb +136 -15
  185. data/app/models/spree/promotion_action.rb +31 -1
  186. data/app/models/spree/promotion_action_line_item.rb +1 -1
  187. data/app/models/spree/promotion_category.rb +1 -1
  188. data/app/models/spree/promotion_handler/cart.rb +1 -1
  189. data/app/models/spree/promotion_handler/coupon.rb +46 -8
  190. data/app/models/spree/promotion_handler/free_shipping.rb +1 -1
  191. data/app/models/spree/promotion_handler/promotion_duplicator.rb +6 -5
  192. data/app/models/spree/promotion_rule.rb +23 -1
  193. data/app/models/spree/promotion_rule_taxon.rb +1 -1
  194. data/app/models/spree/promotion_rule_user.rb +1 -1
  195. data/app/models/spree/property.rb +11 -3
  196. data/app/models/spree/property_prototype.rb +1 -1
  197. data/app/models/spree/prototype.rb +1 -1
  198. data/app/models/spree/prototype_taxon.rb +1 -1
  199. data/app/models/spree/refund.rb +24 -4
  200. data/app/models/spree/refund_reason.rb +12 -2
  201. data/app/models/spree/reimbursement/credit.rb +1 -1
  202. data/app/models/spree/reimbursement/emails.rb +1 -1
  203. data/app/models/spree/reimbursement.rb +5 -3
  204. data/app/models/spree/reimbursement_type.rb +1 -1
  205. data/app/models/spree/report.rb +110 -0
  206. data/app/models/spree/report_line_item.rb +27 -0
  207. data/app/models/spree/report_line_items/products_performance.rb +76 -0
  208. data/app/models/spree/report_line_items/sales_total.rb +49 -0
  209. data/app/models/spree/reports/products_performance.rb +44 -0
  210. data/app/models/spree/reports/sales_total.rb +14 -0
  211. data/app/models/spree/return_authorization.rb +4 -1
  212. data/app/models/spree/return_authorization_reason.rb +1 -1
  213. data/app/models/spree/return_item.rb +2 -1
  214. data/app/models/spree/role.rb +5 -2
  215. data/app/models/spree/role_user.rb +2 -2
  216. data/app/models/spree/shipment/emails.rb +1 -1
  217. data/app/models/spree/shipment/webhooks.rb +1 -1
  218. data/app/models/spree/shipment.rb +14 -4
  219. data/app/models/spree/shipping_category.rb +1 -1
  220. data/app/models/spree/shipping_method.rb +48 -8
  221. data/app/models/spree/shipping_method_category.rb +1 -1
  222. data/app/models/spree/shipping_method_zone.rb +1 -1
  223. data/app/models/spree/shipping_rate.rb +21 -3
  224. data/app/models/spree/state.rb +1 -1
  225. data/app/models/spree/state_change.rb +1 -1
  226. data/app/models/spree/stock_item/webhooks.rb +1 -1
  227. data/app/models/spree/stock_item.rb +9 -6
  228. data/app/models/spree/stock_location.rb +35 -8
  229. data/app/models/spree/stock_movement/webhooks.rb +1 -1
  230. data/app/models/spree/stock_movement.rb +9 -3
  231. data/app/models/spree/stock_transfer.rb +41 -3
  232. data/app/models/spree/store.rb +345 -72
  233. data/app/models/spree/store_credit.rb +12 -4
  234. data/app/models/spree/store_credit_category.rb +1 -1
  235. data/app/models/spree/store_credit_event.rb +21 -1
  236. data/app/models/spree/store_credit_type.rb +1 -1
  237. data/app/models/spree/store_payment_method.rb +1 -1
  238. data/app/models/spree/store_product.rb +1 -1
  239. data/app/models/spree/store_promotion.rb +1 -1
  240. data/app/models/spree/tax_category.rb +15 -1
  241. data/app/models/spree/tax_rate.rb +1 -1
  242. data/app/models/spree/taxon.rb +210 -27
  243. data/app/models/spree/taxon_image/configuration/active_storage.rb +1 -1
  244. data/app/models/spree/taxon_rule.rb +25 -0
  245. data/app/models/spree/taxon_rules/available_on.rb +20 -0
  246. data/app/models/spree/taxon_rules/sale.rb +16 -0
  247. data/app/models/spree/taxon_rules/tag.rb +19 -0
  248. data/app/models/spree/taxonomy.rb +3 -2
  249. data/app/models/spree/theme.rb +200 -0
  250. data/app/models/spree/themes/default.rb +94 -0
  251. data/app/models/spree/variant/webhooks.rb +1 -1
  252. data/app/models/spree/variant.rb +211 -62
  253. data/app/models/spree/wished_item.rb +1 -1
  254. data/app/models/spree/wishlist.rb +1 -1
  255. data/app/models/spree/zone.rb +1 -1
  256. data/app/models/spree/zone_member.rb +1 -1
  257. data/app/presenters/spree/csv/order_line_item_presenter.rb +145 -0
  258. data/app/presenters/spree/csv/product_presenter.rb +63 -0
  259. data/app/presenters/spree/csv/product_variant_presenter.rb +130 -0
  260. data/app/services/spree/addresses/create.rb +31 -6
  261. data/app/services/spree/addresses/helper.rb +7 -4
  262. data/app/services/spree/addresses/update.rb +79 -12
  263. data/app/services/spree/cart/remove_out_of_stock_items.rb +56 -0
  264. data/app/services/spree/checkout/add_store_credit.rb +6 -1
  265. data/app/services/spree/checkout/advance.rb +44 -4
  266. data/app/services/spree/checkout/select_shipping_method.rb +2 -0
  267. data/app/services/spree/coupon_codes/bulk_generate.rb +37 -0
  268. data/app/services/spree/coupon_codes/coupon_codes_handler.rb +34 -0
  269. data/app/services/spree/orders/create_user_account.rb +70 -0
  270. data/app/services/spree/orders/update_contact_information.rb +19 -0
  271. data/app/services/spree/products/auto_match_taxons.rb +64 -0
  272. data/app/services/spree/products/duplicator.rb +121 -0
  273. data/app/services/spree/products/prepare_nested_attributes.rb +169 -0
  274. data/app/services/spree/seeds/admin_user.rb +22 -0
  275. data/app/services/spree/seeds/all.rb +1 -0
  276. data/app/services/spree/seeds/stores.rb +5 -10
  277. data/app/services/spree/seeds/zones.rb +2 -0
  278. data/app/services/spree/tags/bulk_add.rb +38 -0
  279. data/app/services/spree/tags/bulk_remove.rb +24 -0
  280. data/app/services/spree/taxons/add_products.rb +39 -0
  281. data/app/services/spree/taxons/regenerate_products.rb +53 -0
  282. data/app/services/spree/taxons/remove_products.rb +53 -0
  283. data/app/services/spree/taxons/touch_featured_sections.rb +21 -0
  284. data/app/services/spree/themes/duplicator.rb +40 -0
  285. data/app/services/spree/variants/remove_line_items.rb +1 -1
  286. data/app/sorters/spree/classifications/sort.rb +68 -0
  287. data/app/sorters/spree/products/sort.rb +2 -3
  288. data/app/views/action_text/video_embeds/_thumbnail.html.erb +1 -0
  289. data/app/views/action_text/video_embeds/_video_embed.html.erb +3 -0
  290. data/app/views/active_storage/blobs/_blob.html.erb +20 -0
  291. data/app/views/layouts/action_text/contents/_content.html.erb +3 -0
  292. data/app/views/layouts/spree/base_mailer.html.erb +46 -0
  293. data/app/views/spree/addresses/_form.html.erb +58 -0
  294. data/app/views/spree/addresses/_suggestions_box.erb +14 -0
  295. data/app/views/spree/export_mailer/export_done.html.erb +15 -0
  296. data/app/views/spree/report_mailer/report_done.html.erb +15 -0
  297. data/app/views/spree/shared/_base_mailer_footer.html.erb +12 -0
  298. data/app/views/spree/shared/_base_mailer_header.html.erb +6 -0
  299. data/app/views/spree/shared/_base_mailer_stylesheets.html.erb +456 -0
  300. data/app/views/spree/shared/_google_places_api_loader.html.erb +7 -0
  301. data/app/views/spree/shared/_mailer_line_item.html.erb +12 -0
  302. data/app/views/spree/shared/_mailer_logo.html.erb +41 -0
  303. data/app/views/spree/shared/_payment.html.erb +41 -0
  304. data/app/views/spree/test_mailer/test_email.html.erb +40 -0
  305. data/app/views/spree/test_mailer/test_email.text.erb +4 -0
  306. data/config/i18n-tasks.yml +178 -0
  307. data/config/importmap.rb +14 -0
  308. data/config/initializers/acts_as_taggable_on.rb +2 -0
  309. data/config/initializers/inflections.rb +2 -0
  310. data/config/initializers/mobility.rb +4 -0
  311. data/config/initializers/oembed.rb +1 -0
  312. data/config/locales/en.yml +823 -413
  313. data/config/routes.rb +6 -5
  314. data/db/migrate/20210914000000_spree_four_three.rb +0 -79
  315. data/db/migrate/20240913143518_change_default_value_of_spree_option_types_filterable.rb +5 -0
  316. data/db/migrate/20240914153106_add_display_on_to_spree_properties.rb +5 -0
  317. data/db/migrate/20240915144935_add_position_to_spree_properties.rb +6 -0
  318. data/db/migrate/20241004181911_create_spree_coupon_codes.rb +15 -0
  319. data/db/migrate/20241005093437_add_multi_code_to_spree_promotions.rb +14 -0
  320. data/db/migrate/20241014140140_translate_rich_texts.rb +17 -0
  321. data/db/migrate/20241030134309_create_spree_exports.rb +23 -0
  322. data/db/migrate/20241104083457_migrate_spree_promotion_rules_option_value_eligible_values.rb +16 -0
  323. data/db/migrate/20241123110646_add_estimated_transit_fields_to_spree_shipping_methods.rb +6 -0
  324. data/db/migrate/20241127193411_create_spree_taxon_rules.rb +13 -0
  325. data/db/migrate/20241127223627_add_rules_match_policy_and_sort_order_to_spree_taxons.rb +6 -0
  326. data/db/migrate/20241128103947_add_automatic_to_spree_taxons.rb +5 -0
  327. data/db/migrate/20241218183905_add_gateway_customer_profile_id_to_spree_payment_sources.rb +5 -0
  328. data/db/migrate/20250107220432_add_accept_marketing_and_signup_for_an_account_to_spree_orders.rb +6 -0
  329. data/db/migrate/20250110171203_add_quick_checkout_to_spree_addresses.rb +6 -0
  330. data/db/migrate/20250113180019_add_preferences_to_spree_stores.rb +5 -0
  331. data/db/migrate/20250114193857_add_metadata_to_spree_stores.rb +13 -0
  332. data/db/migrate/20250119165904_create_spree_custom_domains.rb +22 -0
  333. data/db/migrate/20250120094216_create_page_builder_models.rb +78 -0
  334. data/db/migrate/20250120152208_add_default_locale_to_action_text_rich_texts.rb +7 -0
  335. data/db/migrate/20250121160028_create_spree_posts_and_spree_post_categories.rb +33 -0
  336. data/db/migrate/20250122113708_add_first_and_last_name_to_spree_admin_class.rb +9 -0
  337. data/db/migrate/20250123135358_add_custom_code_fields_to_spree_store.rb +7 -0
  338. data/db/migrate/20250127083740_add_kind_to_spree_properties.rb +5 -0
  339. data/db/migrate/20250127112758_add_latitude_and_longitude_to_spree_addresses.rb +6 -0
  340. data/db/migrate/20250127151258_add_phone_to_spree_users.rb +5 -0
  341. data/db/migrate/20250204162453_create_spree_reports.rb +13 -0
  342. data/db/migrate/20250207084000_add_accepts_email_marketing_field_to_spree_users_table.rb +6 -0
  343. data/db/migrate/20250217171018_create_action_text_video_embeds.rb +11 -0
  344. data/db/migrate/20250304115943_add_performed_by_to_spree_reimbursements.rb +5 -0
  345. data/db/migrate/20250305121352_remove_page_builder_indices.rb +11 -0
  346. data/db/migrate/20250305121657_remove_spree_posts_indices.rb +7 -0
  347. data/db/migrate/20250311105934_create_spree_gateway_customers.rb +18 -0
  348. data/db/migrate/20250312202705_add_unique_index_on_promotion_rules_associations.rb +75 -0
  349. data/db/migrate/20250313104226_add_user_type_to_spree_role_users.rb +18 -0
  350. data/db/migrate/20250313175830_add_unique_index_on_promotion_action_line_items.rb +29 -0
  351. data/db/migrate/20250314144210_create_spree_taggings_and_spree_tags.rb +38 -0
  352. data/lib/generators/spree/authentication/custom/custom_generator.rb +49 -0
  353. data/lib/generators/spree/authentication/custom/templates/authentication_helpers.rb.tt +50 -0
  354. data/lib/generators/spree/authentication/devise/devise_generator.rb +57 -0
  355. data/lib/generators/spree/authentication/devise/templates/authentication_helpers.rb.tt +50 -0
  356. data/lib/generators/spree/dummy/dummy_generator.rb +1 -1
  357. data/lib/generators/spree/dummy/templates/rails/database.yml +1 -0
  358. data/lib/generators/spree/dummy/templates/rails/test.rb +1 -1
  359. data/lib/generators/spree/install/install_generator.rb +21 -36
  360. data/lib/generators/spree/install/templates/config/initializers/spree.rb +40 -6
  361. data/lib/normalize_string.rb +18 -0
  362. data/lib/spree/analytics.rb +11 -0
  363. data/lib/spree/core/components.rb +1 -1
  364. data/lib/spree/core/configuration.rb +14 -11
  365. data/lib/spree/core/controller_helpers/auth.rb +0 -26
  366. data/lib/spree/core/controller_helpers/order.rb +77 -12
  367. data/lib/spree/core/controller_helpers/search.rb +2 -0
  368. data/lib/spree/core/controller_helpers/store.rb +0 -5
  369. data/lib/spree/core/controller_helpers/strong_parameters.rb +4 -0
  370. data/lib/spree/core/controller_helpers/turbo.rb +19 -0
  371. data/lib/spree/core/dependencies.rb +3 -5
  372. data/lib/spree/core/engine.rb +158 -7
  373. data/lib/spree/core/preferences/configuration.rb +2 -44
  374. data/lib/spree/core/preferences/preferable.rb +9 -0
  375. data/lib/spree/core/preferences/preferable_class_methods.rb +11 -1
  376. data/lib/spree/core/preferences/runtime_configuration.rb +13 -2
  377. data/lib/spree/core/search/base.rb +2 -0
  378. data/lib/spree/core/version.rb +1 -1
  379. data/lib/spree/core.rb +61 -11
  380. data/lib/spree/i18n.rb +5 -8
  381. data/lib/spree/migrations.rb +1 -1
  382. data/lib/spree/permitted_attributes.rb +24 -15
  383. data/lib/spree/testing_support/authorization_helpers.rb +9 -6
  384. data/lib/spree/testing_support/capybara_config.rb +11 -0
  385. data/lib/spree/testing_support/common_rake.rb +20 -12
  386. data/lib/spree/testing_support/factories/calculator_factory.rb +16 -0
  387. data/lib/spree/testing_support/factories/country_factory.rb +8 -0
  388. data/lib/spree/testing_support/factories/coupon_code_factory.rb +7 -0
  389. data/lib/spree/testing_support/factories/custom_domain_factory.rb +7 -0
  390. data/lib/spree/testing_support/factories/export_factory.rb +32 -0
  391. data/lib/spree/testing_support/factories/gateway_customer_factory.rb +7 -0
  392. data/lib/spree/testing_support/factories/order_factory.rb +14 -3
  393. data/lib/spree/testing_support/factories/page_block_factory.rb +21 -0
  394. data/lib/spree/testing_support/factories/page_factory.rb +33 -0
  395. data/lib/spree/testing_support/factories/page_link_factory.rb +7 -0
  396. data/lib/spree/testing_support/factories/page_section_factory.rb +24 -0
  397. data/lib/spree/testing_support/factories/payment_factory.rb +2 -2
  398. data/lib/spree/testing_support/factories/post_category_factory.rb +7 -0
  399. data/lib/spree/testing_support/factories/post_factory.rb +10 -0
  400. data/lib/spree/testing_support/factories/product_factory.rb +3 -8
  401. data/lib/spree/testing_support/factories/product_property_factory.rb +1 -1
  402. data/lib/spree/testing_support/factories/promotion_action_factory.rb +5 -0
  403. data/lib/spree/testing_support/factories/promotion_factory.rb +7 -9
  404. data/lib/spree/testing_support/factories/promotion_rule_factory.rb +12 -0
  405. data/lib/spree/testing_support/factories/property_factory.rb +1 -1
  406. data/lib/spree/testing_support/factories/report_factory.rb +33 -0
  407. data/lib/spree/testing_support/factories/role_user_factory.rb +7 -0
  408. data/lib/spree/testing_support/factories/shipping_method_factory.rb +9 -0
  409. data/lib/spree/testing_support/factories/stock_transfer_factory.rb +5 -2
  410. data/lib/spree/testing_support/factories/store_factory.rb +6 -9
  411. data/lib/spree/testing_support/factories/taxon_factory.rb +13 -0
  412. data/lib/spree/testing_support/factories/taxon_rule_factory.rb +38 -0
  413. data/lib/spree/testing_support/factories/theme_factory.rb +14 -0
  414. data/lib/spree/testing_support/factories/user_factory.rb +13 -11
  415. data/lib/spree/testing_support/factories/variant_factory.rb +23 -5
  416. data/lib/spree/testing_support/jobs.rb +1 -7
  417. data/lib/spree/testing_support/metadata.rb +13 -1
  418. data/lib/spree/testing_support/order_walkthrough.rb +1 -7
  419. data/lib/spree/testing_support/preferences.rb +1 -4
  420. data/lib/spree/testing_support/store.rb +26 -0
  421. data/lib/spree_core.rb +1 -0
  422. data/vendor/javascript/@rails--request.js.js +4 -0
  423. data/vendor/javascript/@stimulus-components--auto-submit.js +4 -0
  424. data/vendor/javascript/stimulus-reveal-controller.js +4 -0
  425. data/vendor/javascript/tailwindcss-stimulus-components.js +4 -0
  426. metadata +340 -197
  427. data/Gemfile +0 -3
  428. data/app/finders/spree/cms_pages/find.rb +0 -41
  429. data/app/finders/spree/menus/find.rb +0 -11
  430. data/app/models/concerns/spree/display_link.rb +0 -30
  431. data/app/models/spree/cms/pages/feature_page.rb +0 -7
  432. data/app/models/spree/cms/pages/homepage.rb +0 -20
  433. data/app/models/spree/cms/pages/standard_page.rb +0 -4
  434. data/app/models/spree/cms/sections/featured_article.rb +0 -22
  435. data/app/models/spree/cms/sections/hero_image.rb +0 -38
  436. data/app/models/spree/cms/sections/image_gallery.rb +0 -97
  437. data/app/models/spree/cms/sections/product_carousel.rb +0 -14
  438. data/app/models/spree/cms/sections/rich_text_content.rb +0 -13
  439. data/app/models/spree/cms/sections/side_by_side_images.rb +0 -68
  440. data/app/models/spree/cms_page.rb +0 -70
  441. data/app/models/spree/cms_section.rb +0 -69
  442. data/app/models/spree/cms_section_image.rb +0 -9
  443. data/app/models/spree/cms_section_image_one.rb +0 -4
  444. data/app/models/spree/cms_section_image_three.rb +0 -4
  445. data/app/models/spree/cms_section_image_two.rb +0 -4
  446. data/app/models/spree/icon.rb +0 -7
  447. data/app/models/spree/linkable/homepage.rb +0 -3
  448. data/app/models/spree/linkable/uri.rb +0 -3
  449. data/app/models/spree/menu.rb +0 -66
  450. data/app/models/spree/menu_item.rb +0 -72
  451. data/brakeman.ignore +0 -328
  452. data/db/migrate/20211201202851_update_linkable_resource_types.rb +0 -10
  453. data/db/migrate/20220329113557_fix_cms_pages_unique_indexes.rb +0 -8
  454. data/lib/generators/spree/custom_authentication/custom_authentication_generator.rb +0 -23
  455. data/lib/generators/spree/custom_authentication/templates/authentication_helpers.rb.tt +0 -23
  456. data/lib/generators/spree/custom_user/custom_user_generator.rb +0 -46
  457. data/lib/generators/spree/custom_user/templates/current_user_helpers.rb.tt +0 -15
  458. data/lib/generators/spree/custom_user/templates/initializer.rb.tt +0 -1
  459. data/lib/generators/spree/custom_user/templates/migration.rb.tt +0 -7
  460. data/lib/generators/spree/dummy/templates/package.json +0 -12
  461. data/lib/spree/core/product_duplicator.rb +0 -85
  462. data/lib/spree/core/runtime_configuration.rb +0 -10
  463. data/lib/spree/testing_support/factories/cms_page_factory.rb +0 -20
  464. data/lib/spree/testing_support/factories/cms_section_factory.rb +0 -31
  465. data/lib/spree/testing_support/factories/icon_factory.rb +0 -9
  466. data/lib/spree/testing_support/factories/menu_factory.rb +0 -16
  467. data/lib/spree/testing_support/factories/menu_item_factory.rb +0 -10
  468. data/script/rails +0 -9
  469. data/spec/fixtures/favicon.ico +0 -0
  470. data/spec/fixtures/files/icon_256x256.gif +0 -0
  471. data/spec/fixtures/files/icon_256x256.png +0 -0
  472. data/spec/fixtures/files/icon_512x512.png +0 -0
  473. data/spec/fixtures/files/img_256x128.png +0 -0
  474. data/spec/fixtures/text-file.txt +0 -1
  475. data/spec/fixtures/thinking-cat.jpg +0 -0
  476. data/spree_core.gemspec +0 -62
@@ -19,7 +19,7 @@
19
19
  #
20
20
 
21
21
  module Spree
22
- class Product < Spree::Base
22
+ class Product < Spree.base_class
23
23
  extend FriendlyId
24
24
  include Spree::ProductScopes
25
25
  include Spree::MultiStoreResource
@@ -34,12 +34,26 @@ module Spree
34
34
 
35
35
  MEMOIZED_METHODS = %w[total_on_hand taxonomy_ids taxon_and_ancestors category
36
36
  default_variant_id tax_category default_variant
37
- purchasable? in_stock? backorderable?]
37
+ default_image secondary_image
38
+ purchasable? in_stock? backorderable? has_variants?]
39
+
40
+ STATUS_TO_WEBHOOK_EVENT = {
41
+ 'active' => 'activated',
42
+ 'draft' => 'drafted',
43
+ 'archived' => 'archived'
44
+ }.freeze
38
45
 
39
46
  TRANSLATABLE_FIELDS = %i[name description slug meta_description meta_keywords meta_title].freeze
40
47
  translates(*TRANSLATABLE_FIELDS, column_fallback: !Spree.always_use_translations?)
41
48
 
42
49
  self::Translation.class_eval do
50
+ if defined?(PgSearch)
51
+ include PgSearch::Model
52
+
53
+ pg_search_scope :search_by_name, against: { name: 'A', meta_title: 'B' },
54
+ using: { tsearch: { prefix: true, any_word: true } }
55
+ end
56
+
43
57
  before_save :set_slug
44
58
  acts_as_paranoid
45
59
  # deleted translation values also need to be accessible for index views listing deleted resources
@@ -61,23 +75,23 @@ module Spree
61
75
  end
62
76
  end
63
77
 
64
- friendly_id :slug_candidates, use: [:history, :mobility]
78
+ friendly_id :slug_candidates, use: [:history, :scoped, :mobility], scope: spree_base_uniqueness_scope
65
79
  acts_as_paranoid
66
80
  auto_strip_attributes :name
81
+ acts_as_taggable_on :tags, :labels
67
82
 
68
83
  # we need to have this callback before any dependent: :destroy associations
69
84
  # https://github.com/rails/rails/issues/3458
70
85
  before_destroy :ensure_not_in_complete_orders
71
86
 
72
- has_many :product_option_types, dependent: :destroy, inverse_of: :product
87
+ has_many :product_option_types, -> { order(:position) }, dependent: :destroy, inverse_of: :product
73
88
  has_many :option_types, through: :product_option_types
74
89
  has_many :product_properties, dependent: :destroy, inverse_of: :product
75
90
  has_many :properties, through: :product_properties
76
91
 
77
- has_many :menu_items, as: :linked_resource
78
-
79
- has_many :classifications, dependent: :delete_all, inverse_of: :product
92
+ has_many :classifications, -> { order(created_at: :asc) }, dependent: :delete_all, inverse_of: :product
80
93
  has_many :taxons, through: :classifications, before_remove: :remove_taxon
94
+ has_many :taxonomies, through: :taxons
81
95
 
82
96
  has_many :product_promotion_rules, class_name: 'Spree::ProductPromotionRule'
83
97
  has_many :promotion_rules, through: :product_promotion_rules, class_name: 'Spree::PromotionRule'
@@ -117,26 +131,37 @@ module Spree
117
131
  has_many :variant_images, -> { order(:position) }, source: :images, through: :variants_including_master
118
132
  has_many :variant_images_without_master, -> { order(:position) }, source: :images, through: :variants
119
133
 
134
+ has_many :option_value_variants, class_name: 'Spree::OptionValueVariant', through: :variants
135
+ has_many :option_values, class_name: 'Spree::OptionValue', through: :variants
136
+
137
+ has_many :prices_including_master, -> { non_zero }, through: :variants_including_master, source: :prices
138
+
120
139
  has_many :store_products, class_name: 'Spree::StoreProduct'
121
140
  has_many :stores, through: :store_products, class_name: 'Spree::Store'
122
141
  has_many :digitals, through: :variants_including_master
123
142
 
143
+ has_many :page_links, as: :linkable, class_name: 'Spree::PageLink', dependent: :destroy
144
+
145
+ after_initialize :ensure_master
146
+ after_initialize :assign_default_tax_category
147
+
148
+ before_validation :downcase_slug
149
+ before_validation :normalize_slug, on: :update
150
+ before_validation :validate_master
151
+ before_validation :ensure_default_shipping_category
152
+
124
153
  after_create :add_associations_from_prototype
125
154
  after_create :build_variants_from_option_values_hash, if: :option_values_hash
126
155
 
127
156
  after_destroy :punch_slug
128
157
  after_restore :update_slug_history
129
158
 
130
- after_initialize :ensure_master
131
-
132
159
  after_save :save_master
133
160
  after_save :run_touch_callbacks, if: :anything_changed?
134
161
  after_save :reset_nested_changes
135
162
  after_touch :touch_taxons
136
163
 
137
- before_validation :downcase_slug
138
- before_validation :normalize_slug, on: :update
139
- before_validation :validate_master
164
+ after_commit :auto_match_taxons, if: :eligible_for_taxon_matching?
140
165
 
141
166
  with_options length: { maximum: 255 }, allow_blank: true do
142
167
  validates :meta_keywords
@@ -152,26 +177,113 @@ module Spree
152
177
  validate :discontinue_on_must_be_later_than_make_active_at, if: -> { make_active_at && discontinue_on }
153
178
 
154
179
  scope :for_store, ->(store) { joins(:store_products).where(StoreProduct.table_name => { store_id: store.id }) }
180
+ scope :draft, -> { where(status: 'draft') }
181
+ scope :archived, -> { where(status: 'archived') }
182
+ scope :not_archived, -> { where.not(status: 'archived') }
183
+ scope :on_sale, lambda { |currency = nil|
184
+ currency ||= Spree::Store.default.default_currency
185
+ joins(:prices_including_master).with_currency(currency).
186
+ where.not(spree_prices: { compare_at_amount: [nil, 0] }).
187
+ where("#{Spree::Price.table_name}.compare_at_amount > #{Spree::Price.table_name}.amount")
188
+ }
189
+
190
+ if defined?(PgSearch)
191
+ scope :multi_search, lambda { |query, include_options = false|
192
+ return none if query.blank?
193
+
194
+ product_ids = if Spree.use_translations?
195
+ Spree::Product::Translation.search_by_name(query).pluck(:spree_product_id)
196
+ else
197
+ Spree::Product.search_by_name(query).ids
198
+ end
199
+
200
+ variant_product_ids = if include_options.present?
201
+ Spree::Variant.search_by_sku_or_options(query).pluck(:product_id)
202
+ else
203
+ Spree::Variant.search_by_sku(query).pluck(:product_id)
204
+ end
205
+
206
+ where(id: (product_ids + variant_product_ids).uniq.compact)
207
+ }
208
+ else
209
+ scope :multi_search, lambda { |query|
210
+ return none if query.blank?
211
+
212
+ product_ids = Spree::Variant.search_by_product_name_or_sku(query).pluck(:product_id)
213
+ where(id: product_ids.uniq.compact)
214
+ }
215
+ end
216
+
217
+ scope :archivable, -> { where(status: %w[active draft]) }
218
+ scope :by_source, ->(source) { send(source) }
219
+ scope :paused, -> { where(status: 'paused') }
220
+ scope :published, -> { where(status: 'active') }
221
+ scope :in_stock_items, -> { joins(:variants).merge(Spree::Variant.in_stock_or_backorderable) }
222
+ scope :out_of_stock_items, lambda {
223
+ joins(variants_including_master: :stock_items).
224
+ where(spree_variants: { track_inventory: true }).
225
+ where.not(id: Spree::Variant.where(track_inventory: false).pluck(:product_id).uniq).
226
+ where(spree_stock_items: { backorderable: false }).
227
+ group(:id).
228
+ having("SUM(#{Spree::StockItem.table_name}.count_on_hand) <= 0")
229
+ }
230
+ scope :out_of_stock, lambda {
231
+ joins(:stock_items).where("#{Spree::StockItem.table_name}.count_on_hand <= ? OR #{Spree::Variant.table_name}.track_inventory = ?", 0, false)
232
+ }
233
+
234
+ scope :by_best_selling, lambda { |order_direction = :desc|
235
+ left_joins(:orders).
236
+ select("#{Spree::Product.table_name}.*, COUNT(#{Spree::Order.table_name}.id) AS completed_orders_count, SUM(#{Spree::Order.table_name}.total) AS completed_orders_total").
237
+ where(Spree::Order.table_name => { id: nil }).
238
+ or(where.not(Spree::Order.table_name => { completed_at: nil })).
239
+ group("#{Spree::Product.table_name}.id").
240
+ order(completed_orders_count: order_direction, completed_orders_total: order_direction)
241
+ }
155
242
 
156
243
  attr_accessor :option_values_hash
157
244
 
158
- accepts_nested_attributes_for :product_properties, allow_destroy: true, reject_if: ->(pp) { pp[:property_name].blank? }
245
+ accepts_nested_attributes_for :product_properties, allow_destroy: true, reject_if: lambda { |pp|
246
+ pp[:property_id].blank? || (pp[:id].blank? && pp[:value].blank?)
247
+ }
248
+ accepts_nested_attributes_for(
249
+ :variants,
250
+ allow_destroy: true,
251
+ reject_if: lambda do |v|
252
+ v[:option_value_variants_attributes].blank? && v[:stock_items_attributes].blank? && v[:prices_attributes].blank?
253
+ end
254
+ )
255
+ accepts_nested_attributes_for :master, reject_if: :all_blank
256
+ accepts_nested_attributes_for(
257
+ :product_option_types,
258
+ allow_destroy: true,
259
+ reject_if: ->(pot) { pot[:option_type_id].blank? || pot[:position].blank? }
260
+ )
159
261
 
160
262
  alias options product_option_types
161
263
 
162
- self.whitelisted_ransackable_associations = %w[taxons stores variants_including_master master variants]
163
264
  self.whitelisted_ransackable_attributes = %w[description name slug discontinue_on status]
164
- self.whitelisted_ransackable_scopes = %w[not_discontinued search_by_name in_taxon price_between]
265
+ self.whitelisted_ransackable_associations = %w[taxons stores variants_including_master master variants tags labels
266
+ shipping_category classifications option_types properties]
267
+ self.whitelisted_ransackable_scopes = %w[not_discontinued search_by_name in_taxon price_between
268
+ multi_search in_stock_items out_of_stock_items]
165
269
 
166
270
  [
167
- :sku, :barcode, :price, :currency, :weight, :height, :width, :depth, :is_master,
168
- :cost_currency, :price_in, :amount_in, :cost_price, :compare_at_price, :compare_at_amount_in
271
+ :sku, :barcode, :weight, :height, :width, :depth, :is_master, :dimensions_unit, :weight_unit
169
272
  ].each do |method_name|
170
273
  delegate method_name, :"#{method_name}=", to: :find_or_build_master
171
274
  end
172
275
 
276
+ [
277
+ :price, :price_in, :amount_in, :compare_at_price, :compare_at_amount_in,
278
+ :currency, :cost_currency, :cost_price, :track_inventory
279
+ ].each do |method_name|
280
+ delegate method_name, :"#{method_name}=", to: :default_variant
281
+ end
282
+
173
283
  delegate :display_amount, :display_price, :has_default_price?,
174
- :display_compare_at_price, :images, to: :find_or_build_master
284
+ :display_compare_at_price, :images, to: :default_variant
285
+
286
+ delegate :name, to: :brand, prefix: true, allow_nil: true
175
287
 
176
288
  alias master_images images
177
289
 
@@ -192,14 +304,31 @@ module Spree
192
304
  after_transition to: :draft, do: [:after_draft, :send_product_drafted_webhook]
193
305
  end
194
306
 
307
+ def self.bulk_auto_match_taxons(store, product_ids)
308
+ return if store.taxons.automatic.none?
309
+
310
+ products_to_auto_match_ids = store.products.not_deleted.not_archived.where(id: product_ids).ids
311
+
312
+ # for ActiveJob 7.1+
313
+ if ActiveJob.respond_to?(:perform_all_later)
314
+ auto_match_taxons_jobs = products_to_auto_match_ids.map do |product_id|
315
+ Spree::Products::AutoMatchTaxonsJob.new(product_id)
316
+ end
317
+
318
+ ActiveJob.perform_all_later(auto_match_taxons_jobs)
319
+ else
320
+ products_to_auto_match_ids.each { |product_id| Spree::Products::AutoMatchTaxonsJob.perform_later(product_id) }
321
+ end
322
+ end
323
+
195
324
  # Can't use short form block syntax due to https://github.com/Netflix/fast_jsonapi/issues/259
196
325
  def purchasable?
197
- default_variant.purchasable? || variants.any?(&:purchasable?)
326
+ @purchasable ||= default_variant.purchasable? || variants.in_stock_or_backorderable.any?
198
327
  end
199
328
 
200
329
  # Can't use short form block syntax due to https://github.com/Netflix/fast_jsonapi/issues/259
201
330
  def in_stock?
202
- default_variant.in_stock? || variants.any?(&:in_stock?)
331
+ @in_stock ||= default_variant.in_stock? || variants.in_stock.any?
203
332
  end
204
333
 
205
334
  # Can't use short form block syntax due to https://github.com/Netflix/fast_jsonapi/issues/259
@@ -207,13 +336,17 @@ module Spree
207
336
  default_variant.backorderable? || variants.any?(&:backorderable?)
208
337
  end
209
338
 
339
+ def on_sale?(currency)
340
+ prices_including_master.find_all { |p| p.currency == currency }.any?(&:discounted?)
341
+ end
342
+
210
343
  def find_or_build_master
211
344
  master || build_master
212
345
  end
213
346
 
214
347
  # the master variant is not a member of the variants array
215
348
  def has_variants?
216
- variants.any?
349
+ @has_variants ||= variants.loaded? ? variants.size.positive? : variants.any?
217
350
  end
218
351
 
219
352
  # Returns default Variant for Product
@@ -225,13 +358,11 @@ module Spree
225
358
  #
226
359
  # @return [Spree::Variant]
227
360
  def default_variant
228
- @default_variant ||= Rails.cache.fetch(default_variant_cache_key) do
229
- if Spree::Config[:track_inventory_levels] && available_variant = variants.detect(&:purchasable?)
230
- available_variant
231
- else
232
- has_variants? ? variants.first : master
233
- end
234
- end
361
+ @default_variant ||= if Spree::Config[:track_inventory_levels] && available_variant = variants.detect(&:purchasable?)
362
+ available_variant
363
+ else
364
+ has_variants? ? variants.first : find_or_build_master
365
+ end
235
366
  end
236
367
 
237
368
  # Returns default Variant ID for Product
@@ -240,19 +371,87 @@ module Spree
240
371
  @default_variant_id ||= default_variant.id
241
372
  end
242
373
 
374
+ # Returns default Image for Product
375
+ # @return [Spree::Image]
376
+ def default_image
377
+ @default_image ||= if images.size.positive?
378
+ images.first
379
+ elsif default_variant.images.size.positive?
380
+ default_variant.default_image
381
+ elsif variant_images.size.positive?
382
+ variant_images.first
383
+ end
384
+ end
385
+ alias featured_image default_image
386
+
387
+ # Returns secondary Image for Product
388
+ # @return [Spree::Image]
389
+ def secondary_image
390
+ @secondary_image ||= if images.size > 1
391
+ images.second
392
+ elsif images.size == 1 && default_variant.images.size.positive?
393
+ default_variant.images.first
394
+ elsif default_variant.images.size > 1
395
+ default_variant.secondary_image
396
+ elsif variant_images.size > 1
397
+ variant_images.second
398
+ end
399
+ end
400
+
401
+ # Returns the short description for the product
402
+ # @return [String]
403
+ def storefront_description
404
+ property('short_description') || description
405
+ end
406
+
407
+ # Returns tax category for Product
408
+ # @return [Spree::TaxCategory]
243
409
  def tax_category
244
- @tax_category ||= super || TaxCategory.find_by(is_default: true)
410
+ @tax_category ||= super || TaxCategory.default
245
411
  end
246
412
 
247
413
  # Adding properties and option types on creation based on a chosen prototype
248
414
  attr_accessor :prototype_id
249
415
 
416
+ def first_or_default_variant(currency)
417
+ if !has_variants?
418
+ default_variant
419
+ elsif first_available_variant(currency).present?
420
+ first_available_variant(currency)
421
+ else
422
+ variants.first
423
+ end
424
+ end
425
+
426
+ def first_available_variant(currency)
427
+ variants.find { |v| v.purchasable? && v.price_in(currency).amount.present? }
428
+ end
429
+
430
+ def price_varies?(currency)
431
+ prices_including_master.find_all { |p| p.currency == currency && p.amount.present? }.map(&:amount).uniq.count > 1
432
+ end
433
+
434
+ def any_variant_available?(currency)
435
+ if has_variants?
436
+ first_available_variant(currency).present?
437
+ else
438
+ master.purchasable? && master.price_in(currency).amount.present?
439
+ end
440
+ end
441
+
442
+ # returns the lowest price for the product in the given currency
443
+ # prices_including_master are usually already loaded, so this should not trigger an extra query
444
+ def lowest_price(currency)
445
+ prices_including_master.find_all { |p| p.currency == currency }.min_by(&:amount)
446
+ end
447
+
250
448
  # Ensures option_types and product_option_types exist for keys in option_values_hash
251
449
  def ensure_option_types_exist_for_values_hash
252
450
  return if option_values_hash.nil?
253
451
 
254
- required_option_type_ids = option_values_hash.keys.map(&:to_i)
255
- missing_option_type_ids = required_option_type_ids - option_type_ids
452
+ # we need to convert the keys to string to make it work with UUIDs
453
+ required_option_type_ids = option_values_hash.keys.map(&:to_s)
454
+ missing_option_type_ids = required_option_type_ids - option_type_ids.map(&:to_s)
256
455
  missing_option_type_ids.each do |id|
257
456
  product_option_types.create(option_type_id: id)
258
457
  end
@@ -261,8 +460,7 @@ module Spree
261
460
  # for adding products which are closely related to existing ones
262
461
  # define "duplicate_extra" for site-specific actions, eg for additional fields
263
462
  def duplicate
264
- duplicator = ProductDuplicator.new(self)
265
- duplicator.duplicate
463
+ Products::Duplicator.call(product: self)
266
464
  end
267
465
 
268
466
  # use deleted? rather than checking the attribute directly. this
@@ -276,7 +474,7 @@ module Spree
276
474
  # deleted products and products with status different than active
277
475
  # are not available
278
476
  def available?
279
- active? && !deleted?
477
+ active? && !deleted? && (available_on.nil? || available_on <= Time.current)
280
478
  end
281
479
 
282
480
  def discontinue!
@@ -330,10 +528,15 @@ module Spree
330
528
  end
331
529
 
332
530
  def property(property_name)
333
- product_properties.joins(:property).find_by(Property.table_name => { name: property_name }).try(:value)
531
+ if product_properties.loaded?
532
+ product_properties.detect { |property| property.property.name == property_name }.try(:value)
533
+ else
534
+ product_properties.joins(:property).find_by(spree_properties: { name: property_name }).try(:value)
535
+ end
334
536
  end
335
537
 
336
538
  def set_property(property_name, property_value, property_presentation = property_name)
539
+ property_name = property_name.to_s.parameterize
337
540
  ApplicationRecord.transaction do
338
541
  # Manual first_or_create to work around Mobility bug
339
542
  property = if Property.where(name: property_name).exists?
@@ -348,7 +551,7 @@ module Spree
348
551
  product_property = if ProductProperty.where(product: self, property: property).exists?
349
552
  ProductProperty.where(product: self, property: property).first
350
553
  else
351
- ProductProperty.create(product: self, property: property)
554
+ ProductProperty.new(product: self, property: property)
352
555
  end
353
556
 
354
557
  product_property.value = property_value
@@ -356,14 +559,16 @@ module Spree
356
559
  end
357
560
  end
358
561
 
562
+ def remove_property(property_name)
563
+ product_properties.joins(:property).find_by(spree_properties: { name: property_name.parameterize })&.destroy
564
+ end
565
+
359
566
  def total_on_hand
360
- @total_on_hand ||= Rails.cache.fetch(['product-total-on-hand', cache_key_with_version]) do
361
- if any_variants_not_track_inventory?
362
- BigDecimal::INFINITY
363
- else
364
- stock_items.sum(:count_on_hand)
365
- end
366
- end
567
+ @total_on_hand ||= if any_variants_not_track_inventory?
568
+ BigDecimal::INFINITY
569
+ else
570
+ stock_items.loaded? ? stock_items.sum(&:count_on_hand) : stock_items.sum(:count_on_hand)
571
+ end
367
572
  end
368
573
 
369
574
  # Master variant may be deleted (i.e. when the product is deleted)
@@ -375,7 +580,7 @@ module Spree
375
580
 
376
581
  def brand
377
582
  @brand ||= if Spree.use_translations?
378
- taxons.joins(:taxonomy).
583
+ taxons.joins(:taxonomy).
379
584
  join_translation_table(Taxonomy).
380
585
  find_by(Taxonomy.translation_table_alias => { name: Spree.t(:taxonomy_brands_name) })
381
586
  else
@@ -394,6 +599,10 @@ module Spree
394
599
  end
395
600
  end
396
601
 
602
+ def main_taxon
603
+ category || taxons.first
604
+ end
605
+
397
606
  def taxons_for_store(store)
398
607
  Rails.cache.fetch("#{cache_key_with_version}/taxons-per-store/#{store.id}") do
399
608
  taxons.for_store(store)
@@ -401,15 +610,60 @@ module Spree
401
610
  end
402
611
 
403
612
  def any_variant_in_stock_or_backorderable?
404
- if variants.any?
613
+ if has_variants?
405
614
  variants_including_master.in_stock_or_backorderable.exists?
406
615
  else
407
616
  master.in_stock_or_backorderable?
408
617
  end
409
618
  end
410
619
 
620
+ # Check if the product is digital by checking if any of its shipping methods are digital delivery
621
+ # This is used to determine if the product is digital and should have a digital delivery price
622
+ # instead of a physical shipping price
623
+ #
624
+ # @return [Boolean]
411
625
  def digital?
412
- shipping_category&.name == I18n.t('spree.seed.shipping.categories.digital')
626
+ shipping_category&.shipping_methods&.any? { |method| method.calculator.is_a?(Spree::Calculator::Shipping::DigitalDelivery) }
627
+ end
628
+
629
+ def auto_match_taxons
630
+ return if deleted?
631
+ return if archived?
632
+
633
+ store = stores.find_by(default: true) || stores.first
634
+ return if store.nil? || store.taxons.automatic.none?
635
+
636
+ Spree::Products::AutoMatchTaxonsJob.perform_later(id)
637
+ end
638
+
639
+ def to_csv(store = nil)
640
+ store ||= stores.default || stores.first
641
+ properties_for_csv ||= Spree::Property.order(:position).flat_map do |property|
642
+ [
643
+ property.name,
644
+ product_properties.find { |pp| pp.property_id == property.id }&.value
645
+ ]
646
+ end
647
+ taxons_for_csv ||= taxons.manual.reorder(depth: :desc).first(3).pluck(:pretty_name)
648
+ taxons_for_csv.fill(nil, taxons_for_csv.size...3)
649
+
650
+ csv_lines = []
651
+
652
+ if has_variants?
653
+ variants_including_master.each_with_index do |variant, index|
654
+ csv_lines << Spree::CSV::ProductVariantPresenter.new(self, variant, index, properties_for_csv, taxons_for_csv, store).call
655
+ end
656
+ else
657
+ csv_lines << Spree::CSV::ProductVariantPresenter.new(self, master, 0, properties_for_csv, taxons_for_csv, store).call
658
+ end
659
+
660
+ csv_lines
661
+ end
662
+
663
+ def page_builder_url
664
+ return unless Spree::Core::Engine.routes.url_helpers.respond_to?(:product_path)
665
+
666
+ Spree::Core::Engine.routes.url_helpers.product_path(self)
413
667
  end
414
668
 
415
669
  private
@@ -450,6 +704,8 @@ module Spree
450
704
  end
451
705
 
452
706
  def default_variant_cache_key
707
+ Spree::Deprecation.warn('Spree::Product#default_variant_cache_key is deprecated and will be removed in Spree 6. Please remove any occurrences of it.')
708
+
453
709
  "spree/default-variant/#{cache_key_with_version}/#{Spree::Config[:track_inventory_levels]}"
454
710
  end
455
711
 
@@ -459,6 +715,10 @@ module Spree
459
715
  self.master ||= build_master
460
716
  end
461
717
 
718
+ def assign_default_tax_category
719
+ self.tax_category = Spree::TaxCategory.default if new_record?
720
+ end
721
+
462
722
  def normalize_slug
463
723
  self.slug = normalize_friendly_id(slug)
464
724
  end
@@ -524,6 +784,15 @@ module Spree
524
784
  end
525
785
  end
526
786
 
787
+ def ensure_default_shipping_category
788
+ return if shipping_category.present?
789
+
790
+ if new_record?
791
+ name = I18n.t('spree.seed.shipping.categories.default')
792
+ self.shipping_category = Spree::ShippingCategory.find_or_create_by!(name: name)
793
+ end
794
+ end
795
+
527
796
  # Try building a slug based on the following fields in increasing order of specificity.
528
797
  def slug_candidates
529
798
  [
@@ -540,15 +809,13 @@ module Spree
540
809
  @taxon_and_ancestors ||= taxons.map(&:self_and_ancestors).flatten.uniq
541
810
  end
542
811
 
543
- # Get the taxonomy ids of all taxons assigned to this product and their ancestors.
544
- def taxonomy_ids
545
- @taxonomy_ids ||= taxon_and_ancestors.map(&:taxonomy_id).flatten.uniq
546
- end
547
-
548
812
  # Iterate through this products taxons and taxonomies and touch their timestamps in a batch
549
813
  def touch_taxons
550
- Spree::Taxon.where(id: taxon_and_ancestors.map(&:id)).update_all(updated_at: Time.current)
551
- Spree::Taxonomy.where(id: taxonomy_ids).update_all(updated_at: Time.current)
814
+ if taxons.any?
815
+ Spree::Products::TouchTaxonsJob.
816
+ set(wait: 5.seconds).
817
+ perform_later(taxon_and_ancestors.map(&:id), taxonomy_ids.uniq)
818
+ end
552
819
  end
553
820
 
554
821
  def ensure_not_in_complete_orders
@@ -560,7 +827,7 @@ module Spree
560
827
 
561
828
  def remove_taxon(taxon)
562
829
  removed_classifications = classifications.where(taxon: taxon)
563
- removed_classifications.each &:remove_from_list
830
+ removed_classifications.each(&:remove_from_list)
564
831
  end
565
832
 
566
833
  def discontinue_on_must_be_later_than_make_active_at
@@ -577,6 +844,10 @@ module Spree
577
844
  true
578
845
  end
579
846
 
847
+ def eligible_for_taxon_matching?
848
+ previously_new_record? || tag_list_previously_changed? || available_on_previously_changed?
849
+ end
850
+
580
851
  def downcase_slug
581
852
  slug&.downcase!
582
853
  end
@@ -1,5 +1,5 @@
1
1
  module Spree
2
- class ProductOptionType < Spree::Base
2
+ class ProductOptionType < Spree.base_class
3
3
  with_options inverse_of: :product_option_types do
4
4
  belongs_to :product, class_name: 'Spree::Product'
5
5
  belongs_to :option_type, class_name: 'Spree::OptionType'
@@ -1,5 +1,5 @@
1
1
  module Spree
2
- class ProductPromotionRule < Spree::Base
2
+ class ProductPromotionRule < Spree.base_class
3
3
  belongs_to :product, class_name: 'Spree::Product'
4
4
  belongs_to :promotion_rule, class_name: 'Spree::PromotionRule'
5
5
 
@@ -1,5 +1,5 @@
1
1
  module Spree
2
- class ProductProperty < Spree::Base
2
+ class ProductProperty < Spree.base_class
3
3
  include Spree::FilterParam
4
4
  include Spree::TranslatableResource
5
5
 
@@ -32,6 +32,9 @@ module Spree
32
32
 
33
33
  scope :filterable, -> { joins(:property).where(Property.table_name => { filterable: true }) }
34
34
  scope :for_products, ->(products) { joins(:product).merge(products) }
35
+ scope :sort_by_property_position, -> {
36
+ joins(:property).order("spree_properties.position ASC")
37
+ }
35
38
 
36
39
  self.whitelisted_ransackable_attributes = ['value', 'filter_param']
37
40
  self.whitelisted_ransackable_associations = ['property']