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
@@ -2,8 +2,11 @@ module Spree
2
2
  class Promotion
3
3
  module Actions
4
4
  class CreateLineItems < PromotionAction
5
- has_many :promotion_action_line_items, foreign_key: :promotion_action_id
6
- accepts_nested_attributes_for :promotion_action_line_items
5
+ has_many :promotion_action_line_items, foreign_key: :promotion_action_id, dependent: :destroy
6
+
7
+ attribute :promotion_action_line_items_attributes
8
+
9
+ after_save :handle_promotion_action_line_items
7
10
 
8
11
  delegate :eligible?, to: :promotion
9
12
 
@@ -79,6 +82,38 @@ module Spree
79
82
  quantifier = Spree::Stock::Quantifier.new(item.variant)
80
83
  quantifier.can_supply? item.quantity
81
84
  end
85
+
86
+ private
87
+
88
+ # Handles the creation and updating of promotion action line items
89
+ #
90
+ # This is a hacky replacement for accepts_nested_attributes_for
91
+ # that allows us to save the PromotionAction and PromotionActionLineItems
92
+ # at the same time.
93
+ def handle_promotion_action_line_items
94
+ return unless promotion_action_line_items_attributes
95
+
96
+ # remove the ones marked for destruction
97
+ ids_for_destruction = promotion_action_line_items_attributes.map { |key, params| params["_destroy"] == "1" ? params["id"] : nil }.compact
98
+ promotion_action_line_items.where(id: ids_for_destruction).delete_all if ids_for_destruction.present?
99
+
100
+ # upsert the rest
101
+ records_for_upsert = promotion_action_line_items_attributes.map { |key, params| params["_destroy"] != "1" ? params : nil }.compact
102
+
103
+ opts = {}
104
+ opts[:unique_by] = [:promotion_action_id, :variant_id] unless ActiveRecord::Base.connection.adapter_name == 'Mysql2'
105
+
106
+ promotion_action_line_items.upsert_all(
107
+ records_for_upsert.map do |params|
108
+ {
109
+ variant_id: params["variant_id"],
110
+ quantity: params["quantity"],
111
+ promotion_action_id: id
112
+ }
113
+ end,
114
+ **opts
115
+ )
116
+ end
82
117
  end
83
118
  end
84
119
  end
@@ -4,18 +4,36 @@ module Spree
4
4
  module Rules
5
5
  class Country < PromotionRule
6
6
  preference :country_id, :integer
7
+ preference :country_iso, :string # Alternative way to configure the rule
7
8
 
8
9
  def applicable?(promotable)
9
10
  promotable.is_a?(Spree::Order)
10
11
  end
11
12
 
12
13
  def eligible?(order, options = {})
13
- country_id = options[:country_id] || order.ship_address.try(:country_id)
14
- unless country_id.eql?(preferred_country_id || order.store.default_country_id)
15
- eligibility_errors.add(:base, eligibility_error_message(:wrong_country))
14
+ if preferred_country_iso.present?
15
+ validate_eligibility_by_country_iso(order)
16
+ else
17
+ validate_eligibility_by_country_id(order, options)
16
18
  end
19
+ end
20
+
21
+ private
22
+
23
+ def validate_eligibility_by_country_id(order, options)
24
+ country_id = options[:country_id] || order.ship_address.try(:country_id)
25
+ return true if country_id == (preferred_country_id || order.store.default_country_id)
26
+
27
+ eligibility_errors.add(:base, eligibility_error_message(:wrong_country))
28
+ false
29
+ end
30
+
31
+ def validate_eligibility_by_country_iso(order)
32
+ country_iso = order.ship_address&.country_iso
33
+ return true if country_iso == (preferred_country_iso || order.store.default_country_iso)
17
34
 
18
- eligibility_errors.empty?
35
+ eligibility_errors.add(:base, eligibility_error_message(:wrong_country))
36
+ false
19
37
  end
20
38
  end
21
39
  end
@@ -0,0 +1,21 @@
1
+ # A rule to limit a promotion based on order currency.
2
+ module Spree
3
+ class Promotion
4
+ module Rules
5
+ class Currency < PromotionRule
6
+ preference :currency, :string
7
+
8
+ def applicable?(promotable)
9
+ promotable.is_a?(Spree::Order)
10
+ end
11
+
12
+ def eligible?(order, options = {})
13
+ return true if order.currency == preferred_currency
14
+
15
+ eligibility_errors.add(:base, eligibility_error_message(:wrong_currency))
16
+ false
17
+ end
18
+ end
19
+ end
20
+ end
21
+ end
@@ -6,7 +6,7 @@ module Spree
6
6
  class ItemTotal < PromotionRule
7
7
  preference :amount_min, :decimal, default: 100.00
8
8
  preference :operator_min, :string, default: '>'
9
- preference :amount_max, :decimal, default: 1000.00
9
+ preference :amount_max, :decimal, default: nil, nullable: true
10
10
  preference :operator_max, :string, default: '<'
11
11
 
12
12
  OPERATORS_MIN = ['gt', 'gte']
@@ -20,7 +20,12 @@ module Spree
20
20
  item_total = order.item_total
21
21
 
22
22
  lower_limit_condition = item_total.send(preferred_operator_min == 'gte' ? :>= : :>, BigDecimal(preferred_amount_min.to_s))
23
- upper_limit_condition = item_total.send(preferred_operator_max == 'lte' ? :<= : :<, BigDecimal(preferred_amount_max.to_s))
23
+
24
+ if preferred_amount_max.present?
25
+ upper_limit_condition = item_total.send(preferred_operator_max == 'lte' ? :<= : :<, BigDecimal(preferred_amount_max.to_s))
26
+ else
27
+ upper_limit_condition = true
28
+ end
24
29
 
25
30
  eligibility_errors.add(:base, ineligible_message_max) unless upper_limit_condition
26
31
  eligibility_errors.add(:base, ineligible_message_min) unless lower_limit_condition
@@ -35,7 +40,11 @@ module Spree
35
40
  end
36
41
 
37
42
  def formatted_amount_max
38
- Spree::Money.new(preferred_amount_max).to_s
43
+ if preferred_amount_max.present?
44
+ Spree::Money.new(preferred_amount_max).to_s
45
+ else
46
+ Spree.t('no_maximum')
47
+ end
39
48
  end
40
49
 
41
50
  def ineligible_message_max
@@ -1,23 +1,12 @@
1
1
  module Spree
2
2
  class Promotion
3
3
  module Rules
4
- module OptionValueWithNumerificationSupport
5
- def preferred_eligible_values
6
- values = super || {}
7
- Hash[values.keys.zip(
8
- values.values.map do |v|
9
- (v.is_a?(Array) ? v : v.split(','))
10
- end
11
- )]
12
- end
13
- end
14
-
15
4
  class OptionValue < PromotionRule
16
- prepend OptionValueWithNumerificationSupport
17
-
18
5
  MATCH_POLICIES = %w(any)
19
6
  preference :match_policy, :string, default: MATCH_POLICIES.first
20
- preference :eligible_values, :hash
7
+ preference :eligible_values, :array, default: [], parse_on_set: lambda { |values|
8
+ values.flat_map { |v| v.to_s.split(',').compact_blank.map(&:strip) }
9
+ }
21
10
 
22
11
  def applicable?(promotable)
23
12
  promotable.is_a?(Spree::Order)
@@ -31,12 +20,8 @@ module Spree
31
20
  end
32
21
 
33
22
  def actionable?(line_item)
34
- product_id = line_item.product_id
35
- option_values_ids = line_item.variant.option_value_ids
36
- eligible_product_ids = preferred_eligible_values.keys
37
- eligible_value_ids = preferred_eligible_values[product_id]
38
-
39
- eligible_product_ids.include?(product_id) && (eligible_value_ids & option_values_ids).present?
23
+ option_value_variant_ids = line_item.variant.option_value_variant_ids.map(&:to_s)
24
+ (preferred_eligible_values & option_value_variant_ids).any?
40
25
  end
41
26
  end
42
27
  end
@@ -5,14 +5,30 @@ module Spree
5
5
  class Promotion
6
6
  module Rules
7
7
  class Product < PromotionRule
8
+ #
9
+ # Associations
10
+ #
8
11
  has_many :product_promotion_rules, class_name: 'Spree::ProductPromotionRule',
9
12
  foreign_key: :promotion_rule_id,
10
13
  dependent: :destroy
11
14
  has_many :products, through: :product_promotion_rules, class_name: 'Spree::Product'
12
15
 
16
+ #
17
+ # Preferences
18
+ #
13
19
  MATCH_POLICIES = %w(any all none)
14
20
  preference :match_policy, :string, default: MATCH_POLICIES.first
15
21
 
22
+ #
23
+ # Attributes
24
+ #
25
+ attr_accessor :product_ids_to_add
26
+
27
+ #
28
+ # Callbacks
29
+ #
30
+ after_save :add_products
31
+
16
32
  # scope/association that is used to test eligibility
17
33
  def eligible_products
18
34
  products
@@ -54,13 +70,29 @@ module Spree
54
70
  end
55
71
 
56
72
  def product_ids_string
73
+ ActiveSupport::Deprecation.warn(
74
+ 'Please use `product_ids=` instead.'
75
+ )
57
76
  product_ids.join(',')
58
77
  end
59
78
 
60
79
  def product_ids_string=(s)
61
- # check this
80
+ ActiveSupport::Deprecation.warn(
81
+ 'Please use `product_ids=` instead.'
82
+ )
62
83
  self.product_ids = s
63
84
  end
85
+
86
+ private
87
+
88
+ def add_products
89
+ return if product_ids_to_add.nil?
90
+
91
+ product_promotion_rules.delete_all
92
+ product_promotion_rules.insert_all(
93
+ product_ids_to_add.map { |product_id| { product_id: product_id, promotion_rule_id: id } }
94
+ )
95
+ end
64
96
  end
65
97
  end
66
98
  end
@@ -2,14 +2,30 @@ module Spree
2
2
  class Promotion
3
3
  module Rules
4
4
  class Taxon < PromotionRule
5
+ #
6
+ # Associations
7
+ #
5
8
  has_many :promotion_rule_taxons, class_name: 'Spree::PromotionRuleTaxon',
6
9
  foreign_key: 'promotion_rule_id',
7
10
  dependent: :destroy
8
11
  has_many :taxons, through: :promotion_rule_taxons, class_name: 'Spree::Taxon'
9
12
 
13
+ #
14
+ # Preferences
15
+ #
10
16
  MATCH_POLICIES = %w(any all)
11
17
  preference :match_policy, default: MATCH_POLICIES.first
12
18
 
19
+ #
20
+ # Attributes
21
+ #
22
+ attr_accessor :taxon_ids_to_add
23
+
24
+ #
25
+ # Callbacks
26
+ #
27
+ after_save :add_taxons
28
+
13
29
  def applicable?(promotable)
14
30
  promotable.is_a?(Spree::Order)
15
31
  end
@@ -39,10 +55,16 @@ module Spree
39
55
  end
40
56
 
41
57
  def taxon_ids_string
58
+ ActiveSupport::Deprecation.warn(
59
+ 'Please use `taxon_ids=` instead.'
60
+ )
42
61
  taxons.pluck(:id).join(',')
43
62
  end
44
63
 
45
64
  def taxon_ids_string=(s)
65
+ ActiveSupport::Deprecation.warn(
66
+ 'Please use `taxon_ids=` instead.'
67
+ )
46
68
  ids = s.to_s.split(',').map(&:strip)
47
69
  self.taxons = Spree::Taxon.for_stores(stores).find(ids)
48
70
  end
@@ -69,6 +91,15 @@ module Spree
69
91
  def taxons_in_order_including_parents(order)
70
92
  order_taxons_in_taxons_and_children(order).inject([]) { |taxons, taxon| taxons << taxon.self_and_ancestors }.flatten.uniq
71
93
  end
94
+
95
+ def add_taxons
96
+ return if taxon_ids_to_add.nil?
97
+
98
+ promotion_rule_taxons.delete_all
99
+ promotion_rule_taxons.insert_all(
100
+ taxon_ids_to_add.map { |taxon_id| { taxon_id: taxon_id, promotion_rule_id: id } }
101
+ )
102
+ end
72
103
  end
73
104
  end
74
105
  end
@@ -2,29 +2,58 @@ module Spree
2
2
  class Promotion
3
3
  module Rules
4
4
  class User < PromotionRule
5
- belongs_to :user, class_name: "::#{Spree.user_class}"
6
-
5
+ #
6
+ # Associations
7
+ #
7
8
  has_many :promotion_rule_users, class_name: 'Spree::PromotionRuleUser',
8
9
  foreign_key: :promotion_rule_id,
9
10
  dependent: :destroy
10
11
  has_many :users, through: :promotion_rule_users, class_name: "::#{Spree.user_class}"
11
12
 
13
+ #
14
+ # Attributes
15
+ #
16
+ attr_accessor :user_ids_to_add
17
+
18
+ #
19
+ # Callbacks
20
+ #
21
+ after_save :add_users
22
+
12
23
  def applicable?(promotable)
13
24
  promotable.is_a?(Spree::Order)
14
25
  end
15
26
 
16
27
  def eligible?(order, _options = {})
17
- users.include?(order.user)
28
+ user_ids.include?(order.user_id)
18
29
  end
19
30
 
20
31
  def user_ids_string
32
+ ActiveSupport::Deprecation.warn(
33
+ 'Spree::Promotion::Rules::User#user_ids_string is deprecated and will be removed in Spree 6.0. ' \
34
+ 'Please use `user_ids` instead.'
35
+ )
21
36
  user_ids.join(',')
22
37
  end
23
38
 
24
39
  def user_ids_string=(s)
25
- # check this
40
+ ActiveSupport::Deprecation.warn(
41
+ 'Spree::Promotion::Rules::User#user_ids_string= is deprecated and will be removed in Spree 6.0. ' \
42
+ 'Please use `user_ids=` instead.'
43
+ )
26
44
  self.user_ids = s
27
45
  end
46
+
47
+ private
48
+
49
+ def add_users
50
+ return if user_ids_to_add.nil?
51
+
52
+ promotion_rule_users.delete_all
53
+ promotion_rule_users.insert_all(
54
+ user_ids_to_add.map { |user_id| { user_id: user_id, promotion_rule_id: id } }
55
+ )
56
+ end
28
57
  end
29
58
  end
30
59
  end
@@ -1,5 +1,5 @@
1
1
  module Spree
2
- class Promotion < Spree::Base
2
+ class Promotion < Spree.base_class
3
3
  include Spree::MultiStoreResource
4
4
  include Spree::Metadata
5
5
  if defined?(Spree::Webhooks::HasWebhooks)
@@ -14,32 +14,64 @@ module Spree
14
14
 
15
15
  attr_reader :eligibility_errors, :generate_code
16
16
 
17
- belongs_to :promotion_category, optional: true
17
+ #
18
+ # Magic methods
19
+ #
20
+ auto_strip_attributes :code, :path, :name
18
21
 
22
+ #
23
+ # Enums
24
+ #
25
+ enum :kind, { coupon_code: 0, automatic: 1 }
26
+
27
+ #
28
+ # Associations
29
+ #
30
+ belongs_to :promotion_category, optional: true
19
31
  has_many :promotion_rules, autosave: true, dependent: :destroy
20
32
  alias rules promotion_rules
21
-
22
33
  has_many :promotion_actions, autosave: true, dependent: :destroy
23
34
  alias actions promotion_actions
24
-
35
+ has_many :coupon_codes, -> { order(created_at: :asc) }, dependent: :destroy, class_name: 'Spree::CouponCode'
25
36
  has_many :order_promotions, class_name: 'Spree::OrderPromotion'
26
37
  has_many :orders, through: :order_promotions, class_name: 'Spree::Order'
27
-
28
38
  has_many :store_promotions, class_name: 'Spree::StorePromotion'
29
39
  has_many :stores, class_name: 'Spree::Store', through: :store_promotions
30
-
31
40
  accepts_nested_attributes_for :promotion_actions, :promotion_rules
32
41
 
42
+ #
43
+ # Callbacks
44
+ #
45
+ before_validation :set_code_to_nil, if: -> { multi_codes? || automatic? }
46
+ before_validation :set_number_of_codes_to_nil, if: -> { automatic? || !multi_codes? }
47
+ before_validation :set_usage_limit_to_nil, if: -> { multi_codes? }
48
+ before_validation :set_kind
49
+ before_validation :downcase_code, if: -> { code.present? }
50
+ before_validation :set_starts_at_to_current_time, if: -> { starts_at.blank? }
51
+ after_commit :generate_coupon_codes, if: -> { multi_codes? }, on: [:create, :update]
52
+ after_commit :remove_coupons, unless: -> { multi_codes? }, on: :update
53
+ before_destroy :not_used?
54
+
55
+ #
56
+ # Validations
57
+ #
33
58
  validates_associated :rules
34
-
35
59
  validates :name, presence: true
36
60
  validates :usage_limit, numericality: { greater_than: 0, allow_nil: true }
37
61
  validates :description, length: { maximum: 255 }, allow_blank: true
38
62
  validate :expires_at_must_be_later_than_starts_at, if: -> { starts_at && expires_at }
39
-
40
- auto_strip_attributes :code, :path, :name
41
-
42
- scope :coupons, -> { where.not(code: nil) }
63
+ validates :code, presence: true, if: -> { coupon_code? && !multi_codes? }
64
+ validates :number_of_codes, numericality: {
65
+ only_integer: true,
66
+ greater_than: 0,
67
+ less_than_or_equal_to: Spree::Config.coupon_codes_total_limit
68
+ }, if: -> { multi_codes? }
69
+
70
+ #
71
+ # Scopes
72
+ #
73
+ scope :expired, -> { where('expires_at < ?', Time.current) }
74
+ scope :coupons, -> { where(kind: :coupon_code) }
43
75
  scope :advertised, -> { where(advertise: true) }
44
76
  scope :applied, lambda {
45
77
  joins(<<-SQL).distinct
@@ -48,12 +80,22 @@ module Spree
48
80
  SQL
49
81
  }
50
82
 
51
- self.whitelisted_ransackable_attributes = ['path', 'promotion_category_id', 'code']
83
+ #
84
+ # Ransack
85
+ #
86
+ self.whitelisted_ransackable_attributes = ['path', 'promotion_category_id', 'code', 'starts_at', 'expires_at']
87
+ self.whitelisted_ransackable_associations = %w[coupon_codes]
52
88
 
53
89
  def self.with_coupon_code(coupon_code)
54
- where("lower(#{table_name}.code) = ?", coupon_code.strip.downcase).
55
- includes(:promotion_actions).where.not(spree_promotion_actions: { id: nil }).
56
- last
90
+ return nil unless coupon_code.present?
91
+
92
+ coupon_code = coupon_code.strip.downcase
93
+
94
+ coupons.includes(:promotion_actions).
95
+ where.not(spree_promotion_actions: { id: nil }).
96
+ where(code: coupon_code).or(
97
+ where(id: Spree::CouponCode.where(code: coupon_code).select(:promotion_id))
98
+ ).last
57
99
  end
58
100
 
59
101
  def self.active
@@ -71,10 +113,22 @@ module Spree
71
113
  end
72
114
  end
73
115
 
116
+ def active?
117
+ starts_at.present? && starts_at < Time.current && (expires_at.blank? || !expired?)
118
+ end
119
+
120
+ def inactive?
121
+ !active?
122
+ end
123
+
74
124
  def expired?
75
125
  !!(starts_at && Time.current < starts_at || expires_at && Time.current > expires_at)
76
126
  end
77
127
 
128
+ def all_codes_used?
129
+ coupon_codes.used.count == coupon_codes.count
130
+ end
131
+
78
132
  def activate(payload)
79
133
  order = payload[:order]
80
134
  return unless self.class.order_activatable?(order)
@@ -209,8 +263,75 @@ module Spree
209
263
  where(spree_adjustments: { source_type: 'Spree::PromotionAction', eligible: true }).any?
210
264
  end
211
265
 
266
+ def name_for_order(order)
267
+ if coupon_code?
268
+ code_for_order(order)
269
+ else
270
+ name
271
+ end.to_s.upcase
272
+ end
273
+
274
+ def code_for_order(order)
275
+ if multi_codes?
276
+ coupon_codes.find_by(order: order)&.code
277
+ else
278
+ code
279
+ end
280
+ end
281
+
212
282
  private
213
283
 
284
+ def not_used?
285
+ return true if orders.empty?
286
+
287
+ errors.add(:base, Spree.t('promotion_already_used'))
288
+ throw(:abort)
289
+ end
290
+
291
+ def set_kind
292
+ if (code.present? || (multi_codes? && number_of_codes.present?)) && kind == 'automatic'
293
+ self.kind = :coupon_code
294
+ end
295
+ end
296
+
297
+ def downcase_code
298
+ self.code = code.downcase.strip
299
+ end
300
+
301
+ def set_code_to_nil
302
+ self.code = nil
303
+ end
304
+
305
+ def set_usage_limit_to_nil
306
+ self.usage_limit = nil
307
+ end
308
+
309
+ def set_number_of_codes_to_nil
310
+ self.number_of_codes = nil
311
+ self.code_prefix = nil
312
+ self.multi_codes = false
313
+ end
314
+
315
+ def set_starts_at_to_current_time
316
+ self.starts_at = Time.current
317
+ end
318
+
319
+ def generate_coupon_codes
320
+ return if number_of_codes.nil?
321
+ return if number_of_codes <= coupon_codes.count
322
+ return unless saved_change_to_number_of_codes?
323
+
324
+ if number_of_codes > Spree::Config.coupon_codes_web_limit
325
+ Spree::CouponCodes::BulkGenerateJob.perform_later(id, number_of_codes - coupon_codes.count)
326
+ else
327
+ Spree::CouponCodes::BulkGenerate.call(promotion: self, quantity: number_of_codes - coupon_codes.count)
328
+ end
329
+ end
330
+
331
+ def remove_coupons
332
+ coupon_codes.where(deleted_at: nil).update_all(deleted_at: Time.current)
333
+ end
334
+
214
335
  def blacklisted?(promotable)
215
336
  case promotable
216
337
  when Spree::LineItem
@@ -1,11 +1,13 @@
1
1
  ## Base class for all types of promotion action.
2
2
  # PromotionActions perform the necessary tasks when a promotion is activated by an event and determined to be eligible.
3
3
  module Spree
4
- class PromotionAction < Spree::Base
4
+ class PromotionAction < Spree.base_class
5
5
  acts_as_paranoid
6
6
 
7
7
  belongs_to :promotion, class_name: 'Spree::Promotion'
8
8
 
9
+ validates :promotion, :type, presence: true
10
+
9
11
  scope :of_type, ->(t) { where(type: t) }
10
12
 
11
13
  # This method should be overridden in subclass
@@ -16,6 +18,34 @@ module Spree
16
18
  raise 'perform should be implemented in a sub-class of PromotionAction'
17
19
  end
18
20
 
21
+ # Returns true if the promotion action is a free shipping action
22
+ #
23
+ # @return [Boolean]
24
+ def free_shipping?
25
+ type == 'Spree::Promotion::Actions::FreeShipping'
26
+ end
27
+
28
+ # Returns the human name of the promotion action
29
+ #
30
+ # @return [String] eg. Free Shipping
31
+ def human_name
32
+ Spree.t("promotion_action_types.#{key}.name")
33
+ end
34
+
35
+ # Returns the human description of the promotion action
36
+ #
37
+ # @return [String]
38
+ def human_description
39
+ Spree.t("promotion_action_types.#{key}.description")
40
+ end
41
+
42
+ # Returns the key of the promotion action
43
+ #
44
+ # @return [String] eg. free_shipping
45
+ def key
46
+ type.demodulize.underscore
47
+ end
48
+
19
49
  protected
20
50
 
21
51
  def label
@@ -1,5 +1,5 @@
1
1
  module Spree
2
- class PromotionActionLineItem < Spree::Base
2
+ class PromotionActionLineItem < Spree.base_class
3
3
  belongs_to :promotion_action, class_name: 'Spree::Promotion::Actions::CreateLineItems'
4
4
  belongs_to :variant, class_name: 'Spree::Variant'
5
5
 
@@ -1,5 +1,5 @@
1
1
  module Spree
2
- class PromotionCategory < Spree::Base
2
+ class PromotionCategory < Spree.base_class
3
3
  validates :name, presence: true
4
4
  has_many :promotions
5
5
  end
@@ -35,7 +35,7 @@ module Spree
35
35
  private
36
36
 
37
37
  def promotions
38
- promotion_scope.find_by_sql("#{order.promotions.active.to_sql} UNION #{promotion_scope.active.where(code: nil, path: nil).to_sql}")
38
+ promotion_scope.find_by_sql("#{order.promotions.active.to_sql} UNION #{promotion_scope.active.automatic.to_sql}")
39
39
  end
40
40
 
41
41
  def promotion_scope