solidus_promotions 4.4.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (234) hide show
  1. checksums.yaml +7 -0
  2. data/.eslintrc.json +10 -0
  3. data/.github/stale.yml +1 -0
  4. data/MIGRATING.md +184 -0
  5. data/README.md +122 -0
  6. data/Rakefile +20 -0
  7. data/app/assets/config/solidus_promotions/manifest.js +13 -0
  8. data/app/decorators/models/solidus_promotions/adjustment_decorator.rb +11 -0
  9. data/app/decorators/models/solidus_promotions/line_item_decorator.rb +27 -0
  10. data/app/decorators/models/solidus_promotions/order_decorator.rb +42 -0
  11. data/app/decorators/models/solidus_promotions/order_recalculator_decorator.rb +15 -0
  12. data/app/decorators/models/solidus_promotions/shipment_decorator.rb +14 -0
  13. data/app/decorators/models/solidus_promotions/shipping_rate_decorator.rb +29 -0
  14. data/app/helpers/solidus_promotions/admin/benefits_helper.rb +19 -0
  15. data/app/helpers/solidus_promotions/admin/conditions_helper.rb +14 -0
  16. data/app/helpers/solidus_promotions/admin/promotions_helper.rb +15 -0
  17. data/app/javascript/backend/solidus_promotions/controllers/application.js +9 -0
  18. data/app/javascript/backend/solidus_promotions/controllers/calculator_tiers_controller.js +37 -0
  19. data/app/javascript/backend/solidus_promotions/controllers/flash_controller.js +10 -0
  20. data/app/javascript/backend/solidus_promotions/controllers/index.js +8 -0
  21. data/app/javascript/backend/solidus_promotions/controllers/product_option_values_controller.js +62 -0
  22. data/app/javascript/backend/solidus_promotions/web_components/number_with_currency.js +35 -0
  23. data/app/javascript/backend/solidus_promotions/web_components/option_value_picker.js +52 -0
  24. data/app/javascript/backend/solidus_promotions/web_components/product_picker.js +7 -0
  25. data/app/javascript/backend/solidus_promotions/web_components/select_two.js +11 -0
  26. data/app/javascript/backend/solidus_promotions/web_components/taxon_picker.js +7 -0
  27. data/app/javascript/backend/solidus_promotions/web_components/user_picker.js +7 -0
  28. data/app/javascript/backend/solidus_promotions/web_components/variant_picker.js +7 -0
  29. data/app/javascript/backend/solidus_promotions.js +11 -0
  30. data/app/jobs/solidus_promotions/promotion_code_batch_job.rb +26 -0
  31. data/app/mailers/solidus_promotions/promotion_code_batch_mailer.rb +15 -0
  32. data/app/models/concerns/solidus_promotions/benefits/line_item_benefit.rb +15 -0
  33. data/app/models/concerns/solidus_promotions/benefits/order_benefit.rb +15 -0
  34. data/app/models/concerns/solidus_promotions/benefits/shipment_benefit.rb +15 -0
  35. data/app/models/concerns/solidus_promotions/calculators/promotion_calculator.rb +11 -0
  36. data/app/models/concerns/solidus_promotions/conditions/line_item_applicable_order_level_condition.rb +23 -0
  37. data/app/models/concerns/solidus_promotions/conditions/line_item_level_condition.rb +15 -0
  38. data/app/models/concerns/solidus_promotions/conditions/order_level_condition.rb +15 -0
  39. data/app/models/concerns/solidus_promotions/conditions/shipment_level_condition.rb +15 -0
  40. data/app/models/concerns/solidus_promotions/discountable_amount.rb +21 -0
  41. data/app/models/solidus_promotions/benefit.rb +123 -0
  42. data/app/models/solidus_promotions/benefits/adjust_line_item.rb +13 -0
  43. data/app/models/solidus_promotions/benefits/adjust_line_item_quantity_groups.rb +107 -0
  44. data/app/models/solidus_promotions/benefits/adjust_shipment.rb +13 -0
  45. data/app/models/solidus_promotions/benefits/create_discounted_item.rb +49 -0
  46. data/app/models/solidus_promotions/calculators/distributed_amount.rb +31 -0
  47. data/app/models/solidus_promotions/calculators/flat_rate.rb +23 -0
  48. data/app/models/solidus_promotions/calculators/flexi_rate.rb +26 -0
  49. data/app/models/solidus_promotions/calculators/percent.rb +19 -0
  50. data/app/models/solidus_promotions/calculators/tiered_flat_rate.rb +58 -0
  51. data/app/models/solidus_promotions/calculators/tiered_percent.rb +68 -0
  52. data/app/models/solidus_promotions/calculators/tiered_percent_on_eligible_item_quantity.rb +40 -0
  53. data/app/models/solidus_promotions/condition.rb +61 -0
  54. data/app/models/solidus_promotions/condition_product.rb +8 -0
  55. data/app/models/solidus_promotions/condition_store.rb +8 -0
  56. data/app/models/solidus_promotions/condition_taxon.rb +8 -0
  57. data/app/models/solidus_promotions/condition_user.rb +8 -0
  58. data/app/models/solidus_promotions/conditions/discounted_item_total.rb +22 -0
  59. data/app/models/solidus_promotions/conditions/first_order.rb +31 -0
  60. data/app/models/solidus_promotions/conditions/first_repeat_purchase_since.rb +31 -0
  61. data/app/models/solidus_promotions/conditions/item_total.rb +75 -0
  62. data/app/models/solidus_promotions/conditions/line_item_option_value.rb +37 -0
  63. data/app/models/solidus_promotions/conditions/line_item_product.rb +56 -0
  64. data/app/models/solidus_promotions/conditions/line_item_taxon.rb +59 -0
  65. data/app/models/solidus_promotions/conditions/minimum_quantity.rb +42 -0
  66. data/app/models/solidus_promotions/conditions/nth_order.rb +40 -0
  67. data/app/models/solidus_promotions/conditions/one_use_per_user.rb +25 -0
  68. data/app/models/solidus_promotions/conditions/option_value.rb +28 -0
  69. data/app/models/solidus_promotions/conditions/product.rb +85 -0
  70. data/app/models/solidus_promotions/conditions/shipping_method.rb +19 -0
  71. data/app/models/solidus_promotions/conditions/store.rb +26 -0
  72. data/app/models/solidus_promotions/conditions/taxon.rb +98 -0
  73. data/app/models/solidus_promotions/conditions/user.rb +35 -0
  74. data/app/models/solidus_promotions/conditions/user_logged_in.rb +16 -0
  75. data/app/models/solidus_promotions/conditions/user_role.rb +42 -0
  76. data/app/models/solidus_promotions/distributed_amounts_handler.rb +39 -0
  77. data/app/models/solidus_promotions/eligibility_result.rb +5 -0
  78. data/app/models/solidus_promotions/eligibility_results.rb +47 -0
  79. data/app/models/solidus_promotions/item_discount.rb +21 -0
  80. data/app/models/solidus_promotions/migration_support/order_promotion_syncer.rb +54 -0
  81. data/app/models/solidus_promotions/order_adjuster/choose_discounts.rb +21 -0
  82. data/app/models/solidus_promotions/order_adjuster/discount_order.rb +93 -0
  83. data/app/models/solidus_promotions/order_adjuster/load_promotions.rb +53 -0
  84. data/app/models/solidus_promotions/order_adjuster/persist_discounted_order.rb +79 -0
  85. data/app/models/solidus_promotions/order_adjuster.rb +33 -0
  86. data/app/models/solidus_promotions/order_promotion.rb +23 -0
  87. data/app/models/solidus_promotions/permission_sets/solidus_promotion_management.rb +15 -0
  88. data/app/models/solidus_promotions/promotion.rb +163 -0
  89. data/app/models/solidus_promotions/promotion_advertiser.rb +10 -0
  90. data/app/models/solidus_promotions/promotion_category.rb +9 -0
  91. data/app/models/solidus_promotions/promotion_code/batch_builder.rb +73 -0
  92. data/app/models/solidus_promotions/promotion_code.rb +56 -0
  93. data/app/models/solidus_promotions/promotion_code_batch.rb +25 -0
  94. data/app/models/solidus_promotions/promotion_finder.rb +10 -0
  95. data/app/models/solidus_promotions/promotion_handler/coupon.rb +113 -0
  96. data/app/models/solidus_promotions/promotion_handler/page.rb +30 -0
  97. data/app/models/solidus_promotions/shipping_rate_discount.rb +11 -0
  98. data/app/subscribers/solidus_promotions/order_promotion_subscriber.rb +20 -0
  99. data/bin/rails +13 -0
  100. data/config/importmap.rb +14 -0
  101. data/config/locales/en.yml +376 -0
  102. data/config/locales/promotion_categories.en.yml +6 -0
  103. data/config/locales/promotions.en.yml +6 -0
  104. data/config/routes.rb +38 -0
  105. data/db/migrate/20230703101637_create_promotions.rb +25 -0
  106. data/db/migrate/20230703113625_create_promotion_benefits.rb +15 -0
  107. data/db/migrate/20230703141116_create_promotion_categories.rb +14 -0
  108. data/db/migrate/20230703143943_create_promotion_conditions.rb +12 -0
  109. data/db/migrate/20230704083830_add_condition_join_tables.rb +31 -0
  110. data/db/migrate/20230704102444_create_promotion_codes.rb +11 -0
  111. data/db/migrate/20230704102656_create_promotion_code_batches.rb +34 -0
  112. data/db/migrate/20230705171556_create_order_promotions.rb +11 -0
  113. data/db/migrate/20230725074235_create_shipping_rate_discounts.rb +12 -0
  114. data/db/migrate/20231011100059_add_db_comments_to_order_promotions.rb +61 -0
  115. data/db/migrate/20231011120928_add_db_comments_to_condition_taxons.rb +54 -0
  116. data/db/migrate/20231011131324_add_db_comments_to_conditions.rb +60 -0
  117. data/db/migrate/20231011142040_add_db_comments_to_condition_users.rb +53 -0
  118. data/db/migrate/20231011155822_add_db_comments_to_promotions.rb +123 -0
  119. data/db/migrate/20231011163030_add_db_comments_to_promotion_codes.rb +60 -0
  120. data/db/migrate/20231011173312_add_db_comments_to_promotion_code_batches.rb +91 -0
  121. data/db/migrate/20231011184205_add_db_comments_to_condition_stores.rb +53 -0
  122. data/db/migrate/20231011190222_add_db_comments_to_benefits.rb +61 -0
  123. data/db/migrate/20231012020928_add_db_comments_to_condition_products.rb +52 -0
  124. data/db/migrate/20231012120928_add_db_comments_to_promotion_categories.rb +52 -0
  125. data/db/migrate/20231104135812_add_managed_by_order_benefit_to_line_items.rb +5 -0
  126. data/lib/components/admin/solidus_admin/orders/show/adjustments/index/source/solidus_promotions_benefit/component.rb +17 -0
  127. data/lib/components/admin/solidus_promotions/orders/index/component.rb +15 -0
  128. data/lib/components/admin/solidus_promotions/orders/index/component.yml +3 -0
  129. data/lib/components/admin/solidus_promotions/promotion_categories/index/component.rb +60 -0
  130. data/lib/components/admin/solidus_promotions/promotions/index/component.rb +108 -0
  131. data/lib/components/admin/solidus_promotions/promotions/index/component.yml +10 -0
  132. data/lib/controllers/admin/solidus_promotions/promotion_categories_controller.rb +29 -0
  133. data/lib/controllers/admin/solidus_promotions/promotions_controller.rb +46 -0
  134. data/lib/controllers/backend/solidus_promotions/admin/base_controller.rb +15 -0
  135. data/lib/controllers/backend/solidus_promotions/admin/benefits_controller.rb +85 -0
  136. data/lib/controllers/backend/solidus_promotions/admin/conditions_controller.rb +69 -0
  137. data/lib/controllers/backend/solidus_promotions/admin/promotion_categories_controller.rb +13 -0
  138. data/lib/controllers/backend/solidus_promotions/admin/promotion_code_batches_controller.rb +42 -0
  139. data/lib/controllers/backend/solidus_promotions/admin/promotion_codes_controller.rb +58 -0
  140. data/lib/controllers/backend/solidus_promotions/admin/promotions_controller.rb +65 -0
  141. data/lib/generators/solidus_promotions/install/install_generator.rb +56 -0
  142. data/lib/generators/solidus_promotions/install/templates/initializer.rb +6 -0
  143. data/lib/solidus_promotions/configuration.rb +125 -0
  144. data/lib/solidus_promotions/engine.rb +115 -0
  145. data/lib/solidus_promotions/migrate_adjustments.rb +62 -0
  146. data/lib/solidus_promotions/migrate_order_promotions.rb +73 -0
  147. data/lib/solidus_promotions/promotion_map.rb +126 -0
  148. data/lib/solidus_promotions/promotion_migrator.rb +103 -0
  149. data/lib/solidus_promotions/testing_support/factories/completed_order_with_solidus_promotion_factory.rb +20 -0
  150. data/lib/solidus_promotions/testing_support/factories/solidus_order_promotion_factory.rb +8 -0
  151. data/lib/solidus_promotions/testing_support/factories/solidus_promotion_category_factory.rb +7 -0
  152. data/lib/solidus_promotions/testing_support/factories/solidus_promotion_code_factory.rb +8 -0
  153. data/lib/solidus_promotions/testing_support/factories/solidus_promotion_factory.rb +65 -0
  154. data/lib/solidus_promotions/testing_support/factories/solidus_shipping_rate_discount_factory.rb +14 -0
  155. data/lib/solidus_promotions/testing_support/factory_bot.rb +30 -0
  156. data/lib/solidus_promotions.rb +37 -0
  157. data/lib/tasks/solidus_promotions/migrate_adjustments.rake +17 -0
  158. data/lib/tasks/solidus_promotions/migrate_existing_promotions.rake +12 -0
  159. data/lib/tasks/solidus_promotions/migrate_order_promotions.rake +17 -0
  160. data/lib/views/backend/solidus_promotions/admin/benefit_fields/_adjust_line_item.html.erb +6 -0
  161. data/lib/views/backend/solidus_promotions/admin/benefit_fields/_adjust_line_item_quantity_groups.html.erb +13 -0
  162. data/lib/views/backend/solidus_promotions/admin/benefit_fields/_adjust_shipment.html.erb +6 -0
  163. data/lib/views/backend/solidus_promotions/admin/benefit_fields/_calculator_fields.erb +11 -0
  164. data/lib/views/backend/solidus_promotions/admin/benefit_fields/_create_discounted_item.html.erb +21 -0
  165. data/lib/views/backend/solidus_promotions/admin/benefits/_benefit.html.erb +50 -0
  166. data/lib/views/backend/solidus_promotions/admin/benefits/_calculator_select.html.erb +16 -0
  167. data/lib/views/backend/solidus_promotions/admin/benefits/_form.html.erb +3 -0
  168. data/lib/views/backend/solidus_promotions/admin/benefits/_new_benefit.html.erb +33 -0
  169. data/lib/views/backend/solidus_promotions/admin/benefits/_type_select.html.erb +16 -0
  170. data/lib/views/backend/solidus_promotions/admin/benefits/edit.html.erb +19 -0
  171. data/lib/views/backend/solidus_promotions/admin/benefits/new.html.erb +1 -0
  172. data/lib/views/backend/solidus_promotions/admin/calculator_fields/_default_fields.html.erb +6 -0
  173. data/lib/views/backend/solidus_promotions/admin/calculator_fields/distributed_amount/_fields.html.erb +18 -0
  174. data/lib/views/backend/solidus_promotions/admin/calculator_fields/flat_rate/_fields.html.erb +6 -0
  175. data/lib/views/backend/solidus_promotions/admin/calculator_fields/flexi_rate/_fields.html.erb +24 -0
  176. data/lib/views/backend/solidus_promotions/admin/calculator_fields/percent/_fields.html.erb +6 -0
  177. data/lib/views/backend/solidus_promotions/admin/calculator_fields/tiered_flat_rate/_fields.html.erb +32 -0
  178. data/lib/views/backend/solidus_promotions/admin/calculator_fields/tiered_flat_rate/_tier_fields.html.erb +31 -0
  179. data/lib/views/backend/solidus_promotions/admin/calculator_fields/tiered_percent/_fields.html.erb +47 -0
  180. data/lib/views/backend/solidus_promotions/admin/calculator_fields/tiered_percent/_tier_fields.html.erb +33 -0
  181. data/lib/views/backend/solidus_promotions/admin/calculator_fields/tiered_percent_on_eligible_item_quantity/_fields.html.erb +36 -0
  182. data/lib/views/backend/solidus_promotions/admin/calculator_fields/tiered_percent_on_eligible_item_quantity/_tier_fields.html.erb +35 -0
  183. data/lib/views/backend/solidus_promotions/admin/condition_fields/_first_order.html.erb +3 -0
  184. data/lib/views/backend/solidus_promotions/admin/condition_fields/_first_repeat_purchase_since.html.erb +7 -0
  185. data/lib/views/backend/solidus_promotions/admin/condition_fields/_item_total.html.erb +15 -0
  186. data/lib/views/backend/solidus_promotions/admin/condition_fields/_line_item_option_value.html.erb +25 -0
  187. data/lib/views/backend/solidus_promotions/admin/condition_fields/_line_item_product.html.erb +21 -0
  188. data/lib/views/backend/solidus_promotions/admin/condition_fields/_line_item_taxon.html.erb +17 -0
  189. data/lib/views/backend/solidus_promotions/admin/condition_fields/_minimum_quantity.html.erb +5 -0
  190. data/lib/views/backend/solidus_promotions/admin/condition_fields/_nth_order.html.erb +15 -0
  191. data/lib/views/backend/solidus_promotions/admin/condition_fields/_one_use_per_user.html.erb +3 -0
  192. data/lib/views/backend/solidus_promotions/admin/condition_fields/_option_value.html.erb +63 -0
  193. data/lib/views/backend/solidus_promotions/admin/condition_fields/_product.html.erb +27 -0
  194. data/lib/views/backend/solidus_promotions/admin/condition_fields/_shipping_method.html.erb +10 -0
  195. data/lib/views/backend/solidus_promotions/admin/condition_fields/_store.html.erb +9 -0
  196. data/lib/views/backend/solidus_promotions/admin/condition_fields/_taxon.html.erb +27 -0
  197. data/lib/views/backend/solidus_promotions/admin/condition_fields/_user.html.erb +7 -0
  198. data/lib/views/backend/solidus_promotions/admin/condition_fields/_user_logged_in.html.erb +3 -0
  199. data/lib/views/backend/solidus_promotions/admin/condition_fields/_user_role.html.erb +15 -0
  200. data/lib/views/backend/solidus_promotions/admin/condition_fields/line_item_option_value/_option_value_fields.html.erb +21 -0
  201. data/lib/views/backend/solidus_promotions/admin/conditions/_condition.html.erb +26 -0
  202. data/lib/views/backend/solidus_promotions/admin/conditions/_new_condition.html.erb +26 -0
  203. data/lib/views/backend/solidus_promotions/admin/conditions/_type_select.html.erb +20 -0
  204. data/lib/views/backend/solidus_promotions/admin/conditions/new.html.erb +1 -0
  205. data/lib/views/backend/solidus_promotions/admin/promotion_categories/_form.html.erb +14 -0
  206. data/lib/views/backend/solidus_promotions/admin/promotion_categories/edit.html.erb +10 -0
  207. data/lib/views/backend/solidus_promotions/admin/promotion_categories/index.html.erb +47 -0
  208. data/lib/views/backend/solidus_promotions/admin/promotion_categories/new.html.erb +10 -0
  209. data/lib/views/backend/solidus_promotions/admin/promotion_code_batches/_form_fields.html.erb +22 -0
  210. data/lib/views/backend/solidus_promotions/admin/promotion_code_batches/download.csv.ruby +8 -0
  211. data/lib/views/backend/solidus_promotions/admin/promotion_code_batches/index.html.erb +65 -0
  212. data/lib/views/backend/solidus_promotions/admin/promotion_code_batches/new.html.erb +8 -0
  213. data/lib/views/backend/solidus_promotions/admin/promotion_codes/index.csv.ruby +8 -0
  214. data/lib/views/backend/solidus_promotions/admin/promotion_codes/index.html.erb +32 -0
  215. data/lib/views/backend/solidus_promotions/admin/promotion_codes/new.html.erb +31 -0
  216. data/lib/views/backend/solidus_promotions/admin/promotions/_form.html.erb +124 -0
  217. data/lib/views/backend/solidus_promotions/admin/promotions/_table.html.erb +57 -0
  218. data/lib/views/backend/solidus_promotions/admin/promotions/_table_filter.html.erb +78 -0
  219. data/lib/views/backend/solidus_promotions/admin/promotions/edit.html.erb +41 -0
  220. data/lib/views/backend/solidus_promotions/admin/promotions/index.html.erb +23 -0
  221. data/lib/views/backend/solidus_promotions/admin/promotions/new.html.erb +9 -0
  222. data/lib/views/backend/solidus_promotions/admin/shared/_number_with_currency.html.erb +20 -0
  223. data/lib/views/backend/solidus_promotions/admin/shared/_promotion_sub_menu.html.erb +14 -0
  224. data/lib/views/backend/solidus_promotions/admin/shared/preference_fields/_boolean.html.erb +14 -0
  225. data/lib/views/backend/solidus_promotions/admin/shared/preference_fields/_decimal.html.erb +12 -0
  226. data/lib/views/backend/solidus_promotions/admin/shared/preference_fields/_encrypted_string.html.erb +12 -0
  227. data/lib/views/backend/solidus_promotions/admin/shared/preference_fields/_integer.html.erb +12 -0
  228. data/lib/views/backend/solidus_promotions/admin/shared/preference_fields/_password.html.erb +12 -0
  229. data/lib/views/backend/solidus_promotions/admin/shared/preference_fields/_string.html.erb +12 -0
  230. data/lib/views/backend/solidus_promotions/admin/shared/preference_fields/_text.html.erb +12 -0
  231. data/lib/views/backend/solidus_promotions/promotion_code_batch_mailer/promotion_code_batch_errored.text.erb +2 -0
  232. data/lib/views/backend/solidus_promotions/promotion_code_batch_mailer/promotion_code_batch_finished.text.erb +2 -0
  233. data/solidus_promotions.gemspec +33 -0
  234. metadata +366 -0
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: db60a6f38884af89ad6588b3b6306f7bda9b9d67170ffeee5b171ed72a0ddafb
4
+ data.tar.gz: 756a0ef51fc6355fa90f5b7824d30d96701b3e9f672da33bf92fa05d845addd3
5
+ SHA512:
6
+ metadata.gz: f8a85cbf84e94bae9702b35cb2dfc376bf5c86f7b144dadfe553637ac51ea8392711cf6fd828773ca358a452f48361a7be5866546a1133fa17cecd46188c75dd
7
+ data.tar.gz: 0fc83043628ca2454361030801fdd08484a638b9e9a0241d422cd453e40bc6d322cde88c0076741cab1371864a9c75ed1b109c981a762daed816399eefceaec6
data/.eslintrc.json ADDED
@@ -0,0 +1,10 @@
1
+ {
2
+ "extends": "eslint:recommended",
3
+ "parserOptions": {
4
+ "ecmaVersion": 2023,
5
+ "sourceType": "module"
6
+ },
7
+ "globals": {
8
+ "Turbo": "readonly"
9
+ }
10
+ }
data/.github/stale.yml ADDED
@@ -0,0 +1 @@
1
+ _extends: .github
data/MIGRATING.md ADDED
@@ -0,0 +1,184 @@
1
+ # Migrating from `solidus_legacy_promotions` to `solidus_promotions`
2
+
3
+ The system is designed to completely replace the legacy promotion system. This guide shows you how
4
+ to run both systems side-by-side, migrate your store's configuration to the `solidus_promotions`, and
5
+ finally remove the dependency on `solidus_legacy_promotions`.
6
+
7
+ Follow these steps to migrate your store to the gem:
8
+
9
+ ## Install solidus_promotions
10
+
11
+ Add the following line to your `Gemfile`:
12
+
13
+ ```rb
14
+ gem "solidus_promotions"
15
+ ```
16
+
17
+ Then run
18
+
19
+ ```sh
20
+ bundle install
21
+ bundle exec rails generate solidus_promotions:install
22
+ ```
23
+
24
+ This will install the extension. It will add new tables, and new routes. It will also change your initializer in `config/initializers/spree.rb`.
25
+
26
+ For the time being, leave the following lines commented out:
27
+
28
+ ```rb
29
+ # Make sure we use Spree::SimpleOrderContents
30
+ # Spree::Config.order_contents_class = "Spree::SimpleOrderContents"
31
+ # Set the promotion configuration to ours
32
+ # Spree::Config.promotions = SolidusPromotions.configuration
33
+ ```
34
+
35
+ This makes sure that the behavior of the current promotion system does not change - yet.
36
+
37
+ ## Migrate existing promotions
38
+
39
+ Now, run the promotion migrator:
40
+
41
+ ```sh
42
+ bundle exec rails solidus_promotions:migrate_existing_promotions
43
+ ```
44
+
45
+ This will create equivalents of the legacy promotion configuration in SolidusPromotions.
46
+
47
+ Now, change `config/initializers/solidus_promotions.rb` to use your new promotion configuration:
48
+
49
+ ## Change store behavior to use SolidusPromotions
50
+
51
+ ```rb
52
+ # Make sure we use Spree::SimpleOrderContents
53
+ Spree::Config.order_contents_class = "Spree::SimpleOrderContents"
54
+ # Set the promotion configuration to ours
55
+ Spree::Config.promotions = SolidusPromotions.configuration
56
+
57
+ # Sync legacy order promotions with the new promotion system
58
+ SolidusPromotions.config.sync_order_promotions = true
59
+ ```
60
+
61
+ From a user's perspective, your promotions should work as before.
62
+
63
+ Before you create new promotions, migrate the adjustments and order promotions in your database:
64
+
65
+ ```rb
66
+ bundle exec rails solidus_promotions:migrate_adjustments:up
67
+ bundle exec rails solidus_promotions:migrate_order_promotions:up
68
+
69
+ ```
70
+
71
+ Check your `spree_adjustments` table for correctness. If things went wrong, you should be able to roll back with
72
+
73
+ ```rb
74
+ bundle exec rails solidus_promotions:migrate_adjustments:down
75
+ bundle exec rails solidus_promotions:migrate_order_promotions:down
76
+ ```
77
+
78
+ Both of these tasks only work if every promotion rule and promotion action have an equivalent condition or benefit in SolidusFrienndlyPromotions. Benefits are connected to their originals promotion action using the `SolidusPromotions#original_promotion_action_id`, Promotions are connected to their originals using the `SolidusPromotions#original_promotion_id`.
79
+
80
+ Once these tasks have run and everything works, you can stop syncing legacy order promotions and new order promotions:
81
+
82
+ ```rb
83
+ SolidusPromotions.config.sync_order_promotions = false
84
+ ```
85
+
86
+ ## Solidus Starter Frontend (and other custom frontends)
87
+
88
+ Stores that have a custom coupon codes controller, such as Solidus' starter frontend, have to change the coupon promotion handler to the one from this gem. Cange any reference to `Spree::PromotionHandler::Coupon` to `Spree::Config.promotions.coupon_code_handler_class`.
89
+
90
+ ## Migrating custom rules and actions
91
+
92
+ If you have custom promotion rules or actions, you need to create new conditions and benefits, respectively.
93
+
94
+ > [!IMPORTANT]
95
+ > SolidusPromotions only supports actions that discount line items and shipments, as well as creating discounted line items. If you have actions that create order-level adjustments, we have no support for that.
96
+
97
+ In our experience, using the three actions can do almost all the things necessary, since they are customizable using calculators.
98
+
99
+ Rules share a lot of the previous API. If you make use of `#actionable?`, you might want to migrate your rule to be a line-item level rule:
100
+
101
+ ```rb
102
+ class MyRule < Spree::PromotionRule
103
+ def actionable?(promotable)
104
+ promotable.quantity > 3
105
+ end
106
+ end
107
+ ```
108
+
109
+ would become:
110
+
111
+ ```rb
112
+ class MyCondition < SolidusPromotions::Condition
113
+ include LineItemLevelCondition
114
+
115
+ def eligible?(promotable)
116
+ promotable.quantity > 3
117
+ end
118
+ end
119
+ ```
120
+
121
+ Now, create your own Promotion conversion map:
122
+
123
+ ```rb
124
+ require 'solidus_promotions/promotion_map'
125
+
126
+ MY_PROMOTION_MAP = SolidusPromotions::PROMOTION_MAP.deep_merge(
127
+ rules: {
128
+ MyRule => MyCondition
129
+ }
130
+ )
131
+ ```
132
+
133
+ The value of the conversion map can also be a callable that takes the original promotion rule and should return a new condition.
134
+
135
+ ```rb
136
+ require 'solidus_promotions/promotion_map'
137
+
138
+ MY_PROMOTION_MAP = SolidusPromotions::PROMOTION_MAP.deep_merge(
139
+ rules: {
140
+ MyRule => ->(old_promotion_rule) {
141
+ MyCondition.new(preferred_quantity: old_promotion_rule.preferred_count)
142
+ }
143
+ }
144
+ )
145
+ ```
146
+
147
+ You can now run our migrator with your map:
148
+
149
+ ```rb
150
+ require 'solidus_promotions/promotion_migrator'
151
+ require 'my_promotion_map'
152
+
153
+ SolidusPromotions::PromotionMigrator.new(MY_PROMOTION_MAP).call
154
+ ```
155
+
156
+ ## Removing `solidus_legacy_promotions`
157
+
158
+ Once your store runs on `solidus_promotions`, you can now drop the dependency on `solidus_legacy_promotions`.
159
+ In order to do so, first make sure you have no ineligible promotion adjustments left in your database:
160
+
161
+ ```rb
162
+ >> Spree::Adjustment.where(eligible: false)
163
+ => 0
164
+ >>
165
+ ```
166
+
167
+ If you still have ineligible adjustments in your database, run the following command:
168
+
169
+ ```sh
170
+ bundle exec rails solidus_legacy_promotions:delete_ineligible_adjustments
171
+ ```
172
+
173
+ Now you can safely remove `solidus_legacy_promotions` from your `Gemfile`. If your store depends on the whole `solidus` suite,
174
+ replace that dependency declaration in the `Gemfile` with the individual gems:
175
+
176
+ ```diff
177
+ # Gemfile
178
+ - gem 'solidus', '~> 4,4'
179
+ + gem 'solidus_core', '~> 4.4'
180
+ + gem 'solidus_api', '~> 4.4'
181
+ + gem 'solidus_backend', '~> 4.4'
182
+ + gem 'solidus_admin', '~> 4.4'
183
+ + gem 'solidus_promotions', '~> 4.4'
184
+ ```
data/README.md ADDED
@@ -0,0 +1,122 @@
1
+ # Solidus Promotions
2
+
3
+ This gem contains Solidus' recommended promotion system. It is slated to replace the promotion system in the `legacy_promotions` gem.
4
+
5
+ The basic architecture is very similar to the legacy promotion system, but with a few decisive tweaks, which I'll explain in the coming sections.
6
+
7
+ ## Architecture
8
+
9
+ This extension centralizes promotion handling in the order updater. A service class, the `SolidusPromotions::OrderAdjuster` applies the current promotion configuration to the order, adjusting or removing adjustments as necessary.
10
+
11
+ `SolidusPromotions::Promotion` objects have benefits, and benefits have conditions. For example, a promotion that is "20% off shirts" would have a benefit of type "AdjustLineItem", and that benefit would have a condition of type "LineItemTaxon" that makes sure only line items with the "shirts" taxon will get the benefit.
12
+
13
+ ### Promotion lanes
14
+
15
+ Promotions get applied by "lane". Promotions within a lane conflict with each other, whereas promotions that do not share a lane will apply sequentially in the order of the lanes. By default these are "pre", "default" and "post", but you can configure this using the SolidusPromotions initializer:
16
+
17
+ ```rb
18
+ SolidusPromotions.configure do |config|
19
+ config.preferred_lanes = {
20
+ pre: 0,
21
+ default: 1,
22
+ grogu: 2,
23
+ post: 3
24
+ }
25
+ end
26
+ ```
27
+
28
+ ### Benefits
29
+
30
+ Solidus Friendly Promotions ships with only three benefit types by default that should cover most use cases: `AdjustLineItem`, `AdjustShipment` and `CreateDiscountedItem`. There is no benefit that creates order-level adjustments, as this feature of Solidus' legacy promotions system has proven to be very difficult for customer service and finance departments due to the difficulty of accruing order-level adjustments to individual line items when e.g. processing returns. In order to give a fixed discount to all line items in an order, use the `AdjustLineItem` benefit with the `DistributedAmount` calculator.
31
+
32
+ Alle benefits are calculable. By setting their `calculator` to one of the classes provided, a great range of discounts is possible.
33
+
34
+ #### `AdjustLineItem`
35
+
36
+ Benefits of this class will create promotion adjustments on line items. By default, they will create a discount on every line item in the order. If you want to restrict which line items get the discount, add line-item level conditions, such as `LineItemProduct`.
37
+
38
+ #### `AdjustShipment`
39
+
40
+ Benefits of this class will create promotion adjustments on shipments. By default, they will create a discount on every shipment in the order. If you want to restrict which shipments get a discount, add shipment-level conditions, such as `ShippingMethod`.
41
+
42
+ ### Conditions
43
+
44
+ Every type of benefit has a list of rules that can be applied to them. When calculating adjustments for an order, benefits will only produce adjustments on line items or shipments if all their respective conditions are true.
45
+
46
+ ### Connecting promotions to orders
47
+
48
+ When there is a join record `SolidusPromotions::OrderPromotion`, the promotion and the order will be "connected", and the promotion will be applied even if it does not `apply_automatically` (see below). This is different from Solidus' legacy promotion system here in that promotions are not automatically connected to orders when they produce an adjustment.
49
+
50
+ If you want to create an `OrderPromotion` record, the usual way to do this is via a promotion handler:
51
+
52
+ - `SolidusPromotions::PromotionHandler::Coupon`: Connects orders to promotions if a customer or service agent enters a matching promotion code.
53
+ - `SolidusPromotions::PromotionHandler::Page`: Connects orders to promotions if a customer visits a page with the correct path. This handler is not integrated in core Solidus, and must be integrated by you.
54
+ - `SolidusPromotions::PromotionHandler::Page`: Connects orders to promotions if a customer visits a page with the correct path. This handler is not integrated in core Solidus, and must be integrated by you.
55
+
56
+ ### Promotion categories
57
+
58
+ Promotion categories simply allow admins to group promotions. They have no further significance with regards to the functionality of the promotion system.
59
+
60
+ ### Promotion recalculation
61
+
62
+ Solidus allows changing orders up until when they are shipped. SolidusPromotions therefore will recalculate orders up until when they are shipped as well. If your admins change promotions rather than add new ones and carefully manage the start and end dates of promotions, you might want to disable this feature:
63
+
64
+ ```rb
65
+ SolidusPromotions.configure do |config|
66
+ config.recalculate_complete_orders = false
67
+ end
68
+ ```
69
+
70
+ ## Installation
71
+
72
+ Add solidus_promotions to your Gemfile:
73
+
74
+ ```ruby
75
+ gem 'solidus_promotions'
76
+ ```
77
+
78
+ Once this project is out of the research phase, a proper gem release will follow.
79
+
80
+ Bundle your dependencies and run the installation generator:
81
+
82
+ ```shell
83
+ bin/rails generate solidus_promotions:install
84
+ ```
85
+
86
+ This will create the tables for this extension. It will also replace the promotion administration system under
87
+ `/admin/promotions` with a new one that needs `turbo-rails`. It will also create an initializer within which Solidus is configured to use `Spree::SimpleOrderContents` and this extension's `OrderAdjuster` classes. Feel free to override with your own implementations!
88
+
89
+ ## Usage
90
+
91
+ Add a promotion using the admin. Add benefits with conditions, and observe promotions getting applied how you'd expect them to.
92
+
93
+ In the admin screen, you can set a number of attributes on your promotion:
94
+ - Name: The name of the promotion. The `name` attribute will also be used to generate adjustment labels. In multi-lingual stores, you probably want different promotions per language for this reason.
95
+
96
+ - Description: This is purely informative. Some stores use `description` in order display information about this promotion to customers, but there is nothing in core Solidus that does it.
97
+
98
+ - Start and End: Outside of the time range between `starts_at` and `expires_at`, the promotion will not be eligible to any order.
99
+
100
+ - Usage Limit: `usage_limit` controls to how many orders this promotion can be applied, independent of promotion code or user. This is most commonly used to limit the risk of losing too much revenue with a particular promotion.
101
+
102
+ - Path: `path` is a URL path that connects the promotion to the order upon visitation. Not currently implemented in either Solidus core or this extension.
103
+
104
+ - Per Code Usage Limit: How often each code can be used. Useful for limiting how many orders can be placed with a single promotion code.
105
+
106
+ - Apply Automatically: Whether this promotion should apply automatically. This means that the promotion is checked for eligibility every time the customer's order is recalculated. Promotion Codes and automatic applications are incompatible.
107
+
108
+ - Promotion Category: Used to group promotions in the admin view.
109
+
110
+ ## Development
111
+
112
+ When testing your application's integration with this extension you may use its factories.
113
+ You can load Solidus core factories along with this extension's factories using this statement:
114
+
115
+ ```ruby
116
+ SolidusDevSupport::TestingSupport::Factories.load_for(SolidusPromotions::Engine)
117
+ ```
118
+
119
+
120
+ ## License
121
+
122
+ Copyright (c) 2024 Martin Meyerhoff, Solidus Team, released under the New BSD License.
data/Rakefile ADDED
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rubygems"
4
+ require "rake"
5
+ require "rake/testtask"
6
+ require "rspec/core/rake_task"
7
+ require "solidus_legacy_promotions"
8
+ require "spree/testing_support/dummy_app/rake_tasks"
9
+ require "solidus_admin/testing_support/dummy_app/rake_tasks"
10
+ require "bundler/gem_tasks"
11
+
12
+ RSpec::Core::RakeTask.new
13
+ task default: :spec
14
+
15
+ DummyApp::RakeTasks.new(
16
+ gem_root: File.dirname(__FILE__),
17
+ lib_name: "solidus_promotions"
18
+ )
19
+
20
+ task test_app: "db:reset"
@@ -0,0 +1,13 @@
1
+ //= link backend/solidus_promotions.js
2
+ //= link backend/solidus_promotions/controllers/application.js
3
+ //= link backend/solidus_promotions/controllers/index.js
4
+ //= link backend/solidus_promotions/controllers/calculator_tiers_controller.js
5
+ //= link backend/solidus_promotions/controllers/flash_controller.js
6
+ //= link backend/solidus_promotions/controllers/product_option_values_controller.js
7
+ //= link backend/solidus_promotions/web_components/option_value_picker.js
8
+ //= link backend/solidus_promotions/web_components/product_picker.js
9
+ //= link backend/solidus_promotions/web_components/user_picker.js
10
+ //= link backend/solidus_promotions/web_components/taxon_picker.js
11
+ //= link backend/solidus_promotions/web_components/variant_picker.js
12
+ //= link backend/solidus_promotions/web_components/number_with_currency.js
13
+ //= link backend/solidus_promotions/web_components/select_two.js
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SolidusPromotions
4
+ module AdjustmentDecorator
5
+ def self.prepended(base)
6
+ base.scope :solidus_promotion, -> { where(source_type: "SolidusPromotions::Benefit") }
7
+ end
8
+
9
+ Spree::Adjustment.prepend self
10
+ end
11
+ end
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SolidusPromotions
4
+ module LineItemDecorator
5
+ def self.prepended(base)
6
+ base.attr_accessor :quantity_setter
7
+ base.belongs_to :managed_by_order_benefit, class_name: "SolidusPromotions::Benefit", optional: true
8
+ base.validate :validate_managed_quantity_same, on: :update
9
+ base.after_save :reset_quantity_setter
10
+ end
11
+
12
+ private
13
+
14
+ def validate_managed_quantity_same
15
+ if managed_by_order_benefit && quantity_changed? && quantity_setter != managed_by_order_benefit
16
+ errors.add(:quantity, :cannot_be_changed_for_automated_items)
17
+ end
18
+ end
19
+
20
+ def reset_quantity_setter
21
+ @quantity_setter = nil
22
+ end
23
+
24
+ Spree::LineItem.prepend self
25
+ Spree::LineItem.prepend SolidusPromotions::DiscountableAmount
26
+ end
27
+ end
@@ -0,0 +1,42 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SolidusPromotions
4
+ module OrderDecorator
5
+ module ClassMethods
6
+ def allowed_ransackable_associations
7
+ super + ["solidus_promotions", "solidus_order_promotions"]
8
+ end
9
+ end
10
+
11
+ def self.prepended(base)
12
+ base.has_many :solidus_order_promotions,
13
+ class_name: "SolidusPromotions::OrderPromotion",
14
+ dependent: :destroy,
15
+ inverse_of: :order
16
+ base.has_many :solidus_promotions, through: :solidus_order_promotions, source: :promotion
17
+ end
18
+
19
+ def discountable_item_total
20
+ line_items.sum(&:discountable_amount)
21
+ end
22
+
23
+ def reset_current_discounts
24
+ line_items.each(&:reset_current_discounts)
25
+ shipments.each(&:reset_current_discounts)
26
+ end
27
+
28
+ # This helper method excludes line items that are managed by an order benefit for the benefit
29
+ # of calculators and benefits that discount normal line items. Line items that are managed by an
30
+ # order benefits handle their discounts themselves.
31
+ def discountable_line_items
32
+ line_items.reject(&:managed_by_order_benefit)
33
+ end
34
+
35
+ def free_from_order_benefit?(line_item, _options)
36
+ !line_item.managed_by_order_benefit
37
+ end
38
+
39
+ Spree::Order.singleton_class.prepend self::ClassMethods
40
+ Spree::Order.prepend self
41
+ end
42
+ end
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SolidusPromotions
4
+ module OrderRecalculatorDecorator
5
+ # This is only needed for stores upgrading from the legacy promotion system.
6
+ # Once we've removed support for the legacy promotion system, we can remove this.
7
+ def recalculate
8
+ if SolidusPromotions.config.sync_order_promotions
9
+ MigrationSupport::OrderPromotionSyncer.new(order: order).call
10
+ end
11
+ super
12
+ end
13
+ Spree::Config.order_recalculator_class.prepend self
14
+ end
15
+ end
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SolidusPromotions
4
+ module ShipmentDecorator
5
+ Spree::Shipment.prepend SolidusPromotions::DiscountableAmount
6
+
7
+ def reset_current_discounts
8
+ super
9
+ shipping_rates.each(&:reset_current_discounts)
10
+ end
11
+
12
+ Spree::Shipment.prepend self
13
+ end
14
+ end
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SolidusPromotions
4
+ module ShippingRateDecorator
5
+ def self.prepended(base)
6
+ base.class_eval do
7
+ has_many :discounts,
8
+ class_name: "SolidusPromotions::ShippingRateDiscount",
9
+ foreign_key: :shipping_rate_id,
10
+ dependent: :destroy,
11
+ inverse_of: :shipping_rate,
12
+ autosave: true
13
+
14
+ money_methods :total_before_tax, :promo_total
15
+ end
16
+ end
17
+
18
+ def total_before_tax
19
+ amount + promo_total
20
+ end
21
+
22
+ def promo_total
23
+ discounts.sum(&:amount)
24
+ end
25
+
26
+ Spree::ShippingRate.prepend SolidusPromotions::DiscountableAmount
27
+ Spree::ShippingRate.prepend self
28
+ end
29
+ end
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SolidusPromotions
4
+ module Admin
5
+ module BenefitsHelper
6
+ def options_for_benefit_calculator_types(benefit)
7
+ calculators = benefit.available_calculators
8
+ options = calculators.map { |calculator| [calculator.model_name.human, calculator.name] }
9
+ options_for_select(options, benefit.calculator_type.to_s)
10
+ end
11
+
12
+ def options_for_benefit_types(benefit)
13
+ benefits = SolidusPromotions.config.benefits
14
+ options = benefits.map { |action| [action.model_name.human, action.name] }
15
+ options_for_select(options, benefit&.type&.to_s)
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SolidusPromotions
4
+ module Admin
5
+ module ConditionsHelper
6
+ def options_for_condition_types(benefit, condition)
7
+ options = benefit.available_conditions.map do |available_condition|
8
+ [available_condition.model_name.human, available_condition.name]
9
+ end
10
+ options_for_select(options, condition&.type.to_s)
11
+ end
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SolidusPromotions
4
+ module Admin
5
+ module PromotionsHelper
6
+ def admin_promotion_status(promotion)
7
+ return :active if promotion.active?
8
+ return :not_started if promotion.not_started?
9
+ return :expired if promotion.expired?
10
+
11
+ :inactive
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,9 @@
1
+ import { Application } from "@hotwired/stimulus";
2
+
3
+ const application = Application.start();
4
+
5
+ // Configure Stimulus development experience
6
+ application.debug = false;
7
+ window.Stimulus = application;
8
+
9
+ export { application };
@@ -0,0 +1,37 @@
1
+ import { Controller } from "@hotwired/stimulus";
2
+
3
+ export default class extends Controller {
4
+ static targets = ["links", "template"];
5
+
6
+ connect() {
7
+ this.wrapperClass = this.data.get("wrapperClass") || "calculator-tiers";
8
+ }
9
+
10
+ add_association(event) {
11
+ event.preventDefault();
12
+
13
+ var content = this.templateTarget.innerHTML;
14
+ this.linksTarget.insertAdjacentHTML("beforebegin", content);
15
+ }
16
+
17
+ propagate_base_to_value_input(event) {
18
+ event.preventDefault();
19
+
20
+ // targets the content of the last pair of square brackets
21
+ // we first need to greedily match all other square brackets
22
+ const regEx = /(\[.*\])\[.*?\]$/;
23
+ let wrapper = event.target.closest("." + this.wrapperClass);
24
+ let valueInput = wrapper.querySelector(".js-value-input");
25
+ valueInput.name = valueInput.name.replace(
26
+ regEx,
27
+ `$1[${event.target.value}]`
28
+ );
29
+ }
30
+
31
+ remove_association(event) {
32
+ event.preventDefault();
33
+
34
+ let wrapper = event.target.closest("." + this.wrapperClass);
35
+ wrapper.remove();
36
+ }
37
+ }
@@ -0,0 +1,10 @@
1
+ import { Controller } from "@hotwired/stimulus";
2
+
3
+ export default class extends Controller {
4
+ connect() {
5
+ window.show_flash(
6
+ this.element.dataset.severity,
7
+ this.element.dataset.message
8
+ );
9
+ }
10
+ }
@@ -0,0 +1,8 @@
1
+ import { application } from "backend/solidus_promotions/controllers/application";
2
+
3
+ // Eager load all controllers defined in the import map under controllers/**/*_controller
4
+ import { eagerLoadControllersFrom } from "@hotwired/stimulus-loading";
5
+ eagerLoadControllersFrom(
6
+ "backend/solidus_promotions/controllers",
7
+ application
8
+ );