solidus_promotions 4.4.0

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 (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
@@ -0,0 +1,62 @@
1
+ import { Controller } from "@hotwired/stimulus";
2
+
3
+ export default class extends Controller {
4
+ static targets = ["links", "template"];
5
+
6
+ connect() {
7
+ this.wrapperClass =
8
+ this.data.get("wrapperClass") || "promo-condition-option-value";
9
+
10
+ this.element.querySelectorAll("." + this.wrapperClass).forEach((element) => this.buildSelects(element))
11
+ }
12
+
13
+ add_row(event) {
14
+ event.preventDefault();
15
+
16
+ var content = this.templateTarget.innerHTML;
17
+ this.linksTarget.insertAdjacentHTML("beforebegin", content);
18
+ this.buildSelects(this.linksTarget.previousElementSibling)
19
+ }
20
+
21
+ propagate_product_id_to_value_input(event) {
22
+ event.preventDefault();
23
+ // targets the content of the last pair of square brackets
24
+ // we first need to greedily match all other square brackets
25
+ const regEx = /(\[.*\])\[.*?\]$/;
26
+ let wrapper = event.target.closest("." + this.wrapperClass);
27
+ let optionValuesInput = wrapper.querySelector(".option-values-select[type='hidden']");
28
+ optionValuesInput.name = optionValuesInput.name.replace(
29
+ regEx,
30
+ `$1[${event.target.value}]`
31
+ );
32
+ }
33
+
34
+ remove_row(event) {
35
+ event.preventDefault();
36
+
37
+ let wrapper = event.target.closest("." + this.wrapperClass);
38
+ wrapper.remove();
39
+ }
40
+
41
+ // helper functions
42
+
43
+ buildSelects(wrapper) {
44
+ let productSelect = wrapper.querySelector(".product-select")
45
+ let optionValueSelect = wrapper.querySelector(".option-values-select[type='hidden']")
46
+ this.buildProductSelect(productSelect)
47
+ $(optionValueSelect).optionValueAutocomplete({ productSelect });
48
+ }
49
+
50
+ buildProductSelect(productSelect) {
51
+ var jQueryProductSelect = $(productSelect)
52
+ jQueryProductSelect.productAutocomplete({
53
+ multiple: false,
54
+ })
55
+ // capture the jQuery "change" event and re-emit it as DOM event "select2Change"
56
+ // so that Stimulus can capture it
57
+ jQueryProductSelect.on('change', function () {
58
+ let event = new Event('select2Change', { bubbles: true }) // fire a native event
59
+ productSelect.dispatchEvent(event);
60
+ });
61
+ }
62
+ }
@@ -0,0 +1,35 @@
1
+ class NumberWithCurrency extends HTMLElement {
2
+ connectedCallback() {
3
+ this.currencySelector = this.querySelector('.number-with-currency-select');
4
+ this.render()
5
+ this.addEventListener('change', this);
6
+ }
7
+
8
+ handleEvent() {
9
+ this.render()
10
+ }
11
+
12
+ get currency() {
13
+ if (this.currencySelector) {
14
+ return this.currencySelector.value;
15
+ } else {
16
+ return this.querySelector('.number-with-currency-addon')?.dataset?.currency;
17
+ }
18
+ }
19
+
20
+ get currencySymbol() {
21
+ const currency = this.currency;
22
+ if (currency) {
23
+ const currencyInfo = Spree.currencyInfo[currency];
24
+ return currencyInfo[0];
25
+ } else {
26
+ return '';
27
+ }
28
+ }
29
+
30
+ render() {
31
+ this.querySelector('.number-with-currency-symbol').textContent = this.currencySymbol;
32
+ }
33
+ }
34
+
35
+ customElements.define('number-with-currency', NumberWithCurrency);
@@ -0,0 +1,52 @@
1
+ $.fn.optionValueAutocomplete = function (options) {
2
+ 'use strict';
3
+
4
+ // Default options
5
+ options = options || {}
6
+ var multiple = typeof(options['multiple']) !== 'undefined' ? options['multiple'] : true;
7
+ var productSelect = options['productSelect'];
8
+
9
+ function formatOptionValue(option_value) {
10
+ return Select2.util.escapeMarkup(option_value.name);
11
+ }
12
+
13
+ this.select2({
14
+ minimumInputLength: 3,
15
+ multiple: multiple,
16
+ initSelection: function (element, callback) {
17
+ $.get(Spree.pathFor('api/option_values'), {
18
+ ids: element.val().split(','),
19
+ token: Spree.api_key
20
+ }, function (data) {
21
+ callback(multiple ? data : data[0]);
22
+ });
23
+ },
24
+ ajax: {
25
+ url: Spree.pathFor('api/option_values'),
26
+ datatype: 'json',
27
+ data: function (term, page) {
28
+ var productId = typeof(productSelect) !== 'undefined' ? $(productSelect).select2('val') : null;
29
+ return {
30
+ q: {
31
+ name_cont: term,
32
+ variants_product_id_eq: productId
33
+ },
34
+ token: Spree.api_key
35
+ };
36
+ },
37
+ results: function (data, page) {
38
+ return { results: data };
39
+ }
40
+ },
41
+ formatResult: formatOptionValue,
42
+ formatSelection: formatOptionValue
43
+ });
44
+ };
45
+
46
+ class OptionValuePicker extends HTMLInputElement {
47
+ connectedCallback() {
48
+ $(this).optionValueAutocomplete();
49
+ }
50
+ }
51
+
52
+ customElements.define('option-value-picker', OptionValuePicker, { extends: 'input' });
@@ -0,0 +1,7 @@
1
+ class ProductPicker extends HTMLInputElement {
2
+ connectedCallback() {
3
+ $(this).productAutocomplete();
4
+ }
5
+ }
6
+
7
+ customElements.define('product-picker', ProductPicker, { extends: 'input' });
@@ -0,0 +1,11 @@
1
+ class SelectTwo extends HTMLSelectElement {
2
+ connectedCallback() {
3
+ $(this).select2({
4
+ allowClear: true,
5
+ dropdownAutoWidth: true,
6
+ minimumResultsForSearch: 8,
7
+ });
8
+ }
9
+ }
10
+
11
+ customElements.define("select-two", SelectTwo, { extends: "select" });
@@ -0,0 +1,7 @@
1
+ class TaxonPicker extends HTMLInputElement {
2
+ connectedCallback() {
3
+ $(this).taxonAutocomplete();
4
+ }
5
+ }
6
+
7
+ customElements.define('taxon-picker', TaxonPicker, { extends: 'input' });
@@ -0,0 +1,7 @@
1
+ class UserPicker extends HTMLInputElement {
2
+ connectedCallback() {
3
+ $(this).userAutocomplete();
4
+ }
5
+ }
6
+
7
+ customElements.define('user-picker', UserPicker, { extends: 'input' });
@@ -0,0 +1,7 @@
1
+ class VariantPicker extends HTMLInputElement {
2
+ connectedCallback() {
3
+ $(this).variantAutocomplete();
4
+ }
5
+ }
6
+
7
+ customElements.define('variant-picker', VariantPicker, { extends: 'input' });
@@ -0,0 +1,11 @@
1
+ import "@hotwired/turbo-rails";
2
+ import "backend/solidus_promotions/controllers";
3
+ import "backend/solidus_promotions/web_components/option_value_picker"
4
+ import "backend/solidus_promotions/web_components/product_picker"
5
+ import "backend/solidus_promotions/web_components/user_picker"
6
+ import "backend/solidus_promotions/web_components/taxon_picker"
7
+ import "backend/solidus_promotions/web_components/variant_picker"
8
+ import "backend/solidus_promotions/web_components/number_with_currency"
9
+ import "backend/solidus_promotions/web_components/select_two"
10
+
11
+ Turbo.session.drive = false;
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SolidusPromotions
4
+ class PromotionCodeBatchJob < ActiveJob::Base
5
+ queue_as :default
6
+
7
+ def perform(promotion_code_batch)
8
+ PromotionCode::BatchBuilder.new(
9
+ promotion_code_batch
10
+ ).build_promotion_codes
11
+
12
+ if promotion_code_batch.email?
13
+ SolidusPromotions.config.promotion_code_batch_mailer_class
14
+ .promotion_code_batch_finished(promotion_code_batch)
15
+ .deliver_now
16
+ end
17
+ rescue StandardError => e
18
+ if promotion_code_batch.email?
19
+ SolidusPromotions.config.promotion_code_batch_mailer_class
20
+ .promotion_code_batch_errored(promotion_code_batch)
21
+ .deliver_now
22
+ end
23
+ raise e
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SolidusPromotions
4
+ class PromotionCodeBatchMailer < Spree::BaseMailer
5
+ def promotion_code_batch_finished(promotion_code_batch)
6
+ @promotion_code_batch = promotion_code_batch
7
+ mail(to: promotion_code_batch.email)
8
+ end
9
+
10
+ def promotion_code_batch_errored(promotion_code_batch)
11
+ @promotion_code_batch = promotion_code_batch
12
+ mail(to: promotion_code_batch.email)
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SolidusPromotions
4
+ module Benefits
5
+ module LineItemBenefit
6
+ def can_discount?(object)
7
+ object.is_a? Spree::LineItem
8
+ end
9
+
10
+ def level
11
+ :line_item
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SolidusPromotions
4
+ module Benefits
5
+ module OrderBenefit
6
+ def can_discount?(_)
7
+ false
8
+ end
9
+
10
+ def level
11
+ :order
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SolidusPromotions
4
+ module Benefits
5
+ module ShipmentBenefit
6
+ def can_discount?(object)
7
+ object.is_a?(Spree::Shipment) || object.is_a?(Spree::ShippingRate)
8
+ end
9
+
10
+ def level
11
+ :shipment
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SolidusPromotions
4
+ module Calculators
5
+ module PromotionCalculator
6
+ def description
7
+ self.class.human_attribute_name(:description)
8
+ end
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SolidusPromotions
4
+ module Conditions
5
+ module LineItemApplicableOrderLevelCondition
6
+ def self.included(klass)
7
+ klass.preference :line_item_applicable, :boolean, default: true
8
+ end
9
+
10
+ def applicable?(promotable)
11
+ promotable.is_a?(Spree::Order) || preferred_line_item_applicable && promotable.is_a?(Spree::LineItem)
12
+ end
13
+
14
+ def eligible?(promotable)
15
+ send(:"#{promotable.class.name.demodulize.underscore}_eligible?", promotable)
16
+ end
17
+
18
+ def level
19
+ :order
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SolidusPromotions
4
+ module Conditions
5
+ module LineItemLevelCondition
6
+ def applicable?(promotable)
7
+ promotable.is_a?(Spree::LineItem)
8
+ end
9
+
10
+ def level
11
+ :line_item
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SolidusPromotions
4
+ module Conditions
5
+ module OrderLevelCondition
6
+ def applicable?(promotable)
7
+ promotable.is_a?(Spree::Order)
8
+ end
9
+
10
+ def level
11
+ :order
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SolidusPromotions
4
+ module Conditions
5
+ module ShipmentLevelCondition
6
+ def applicable?(promotable)
7
+ promotable.is_a?(Spree::Shipment)
8
+ end
9
+
10
+ def level
11
+ :shipment
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SolidusPromotions
4
+ module DiscountableAmount
5
+ def discountable_amount
6
+ amount + current_discounts.sum(&:amount)
7
+ end
8
+
9
+ def current_discounts
10
+ @current_discounts ||= []
11
+ end
12
+
13
+ def current_discounts=(args)
14
+ @current_discounts = args
15
+ end
16
+
17
+ def reset_current_discounts
18
+ @current_discounts = []
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,123 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "spree/preferences/persistable"
4
+
5
+ module SolidusPromotions
6
+ # Base class for all types of benefit.
7
+ #
8
+ # Benefits perform the necessary tasks when a promotion is activated
9
+ # by an event and determined to be eligible.
10
+ class Benefit < Spree::Base
11
+ include Spree::Preferences::Persistable
12
+ include Spree::CalculatedAdjustments
13
+ include Spree::AdjustmentSource
14
+ before_destroy :remove_adjustments_from_incomplete_orders
15
+ before_destroy :raise_for_adjustments_for_completed_orders
16
+
17
+ belongs_to :promotion, inverse_of: :benefits
18
+ belongs_to :original_promotion_action, class_name: "Spree::PromotionAction", optional: true
19
+ has_many :adjustments, class_name: "Spree::Adjustment", as: :source
20
+ has_many :shipping_rate_discounts, class_name: "SolidusPromotions::ShippingRateDiscount", inverse_of: :benefit
21
+ has_many :conditions, class_name: "SolidusPromotions::Condition", inverse_of: :benefit, dependent: :destroy
22
+
23
+ scope :of_type, ->(type) { where(type: Array.wrap(type).map(&:to_s)) }
24
+
25
+ def preload_relations
26
+ [:calculator]
27
+ end
28
+
29
+ def can_discount?(object)
30
+ raise NotImplementedError, "Please implement the correct interface, or include one of the `SolidusPromotions::Benefits::OrderBenefit`, " \
31
+ "`SolidusPromotions::Benefits::LineItemBenefit` or `SolidusPromotions::Benefits::ShipmentBenefit` modules"
32
+ end
33
+
34
+ def discount(adjustable)
35
+ amount = compute_amount(adjustable)
36
+ return if amount.zero?
37
+ ItemDiscount.new(
38
+ item: adjustable,
39
+ label: adjustment_label(adjustable),
40
+ amount: amount,
41
+ source: self
42
+ )
43
+ end
44
+
45
+ # Ensure a negative amount which does not exceed the object's amount
46
+ def compute_amount(adjustable)
47
+ promotion_amount = calculator.compute(adjustable) || BigDecimal("0")
48
+ [adjustable.discountable_amount, promotion_amount.abs].min * -1
49
+ end
50
+
51
+ def adjustment_label(adjustable)
52
+ I18n.t(
53
+ "solidus_promotions.adjustment_labels.#{adjustable.class.name.demodulize.underscore}",
54
+ promotion: SolidusPromotions::Promotion.model_name.human,
55
+ promotion_customer_label: promotion.customer_label
56
+ )
57
+ end
58
+
59
+ def to_partial_path
60
+ "solidus_promotions/admin/benefit_fields/#{model_name.element}"
61
+ end
62
+
63
+ def level
64
+ raise NotImplementedError, "Please implement the correct interface, or include one of the `SolidusPromotions::Benefits::OrderBenefit`, " \
65
+ "`SolidusPromotions::Benefits::LineItemBenefit` or `SolidusPromotions::Benefits::ShipmentBenefit` modules"
66
+ end
67
+
68
+ def available_conditions
69
+ possible_conditions - conditions.select(&:persisted?)
70
+ end
71
+
72
+ def available_calculators
73
+ SolidusPromotions.config.promotion_calculators[self.class] || []
74
+ end
75
+
76
+ def eligible_by_applicable_conditions?(promotable, dry_run: false)
77
+ applicable_conditions = conditions.select do |condition|
78
+ condition.applicable?(promotable)
79
+ end
80
+
81
+ applicable_conditions.map do |applicable_condition|
82
+ eligible = applicable_condition.eligible?(promotable)
83
+
84
+ break [false] if !eligible && !dry_run
85
+
86
+ if dry_run
87
+ if applicable_condition.eligibility_errors.details[:base].first
88
+ code = applicable_condition.eligibility_errors.details[:base].first[:error_code]
89
+ message = applicable_condition.eligibility_errors.full_messages.first
90
+ end
91
+ promotion.eligibility_results.add(
92
+ item: promotable,
93
+ condition: applicable_condition,
94
+ success: eligible,
95
+ code: eligible ? nil : (code || :coupon_code_unknown_error),
96
+ message: eligible ? nil : (message || I18n.t(:coupon_code_unknown_error, scope: [:solidus_promotions, :eligibility_errors]))
97
+ )
98
+ end
99
+
100
+ eligible
101
+ end.all?
102
+ end
103
+
104
+ def applicable_line_items(order)
105
+ order.discountable_line_items.select do |line_item|
106
+ eligible_by_applicable_conditions?(line_item)
107
+ end
108
+ end
109
+
110
+ def possible_conditions
111
+ Set.new(SolidusPromotions.config.order_conditions)
112
+ end
113
+
114
+ private
115
+
116
+ def raise_for_adjustments_for_completed_orders
117
+ if adjustments.joins(:order).merge(Spree::Order.complete).any?
118
+ errors.add(:base, :cannot_destroy_if_order_completed)
119
+ throw(:abort)
120
+ end
121
+ end
122
+ end
123
+ end
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SolidusPromotions
4
+ module Benefits
5
+ class AdjustLineItem < Benefit
6
+ include SolidusPromotions::Benefits::LineItemBenefit
7
+
8
+ def possible_conditions
9
+ super + SolidusPromotions.config.line_item_conditions
10
+ end
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,107 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SolidusPromotions
4
+ module Benefits
5
+ class AdjustLineItemQuantityGroups < AdjustLineItem
6
+ preference :group_size, :integer, default: 1
7
+
8
+ ##
9
+ # Computes the amount for the adjustment based on the line item and any
10
+ # other applicable items in the order. The conditions for this specific
11
+ # adjustment are as follows:
12
+ #
13
+ # = Setup
14
+ #
15
+ # We have a quantity group promotion on t-shirts. If a user orders 3
16
+ # t-shirts, they get $5 off of each. The shirts come in one size and three
17
+ # colours: red, blue, and white.
18
+ #
19
+ # == Scenario 1
20
+ #
21
+ # User has 2 red shirts, 1 white shirt, and 1 blue shirt in their
22
+ # order. We want to compute the adjustment amount for the white shirt.
23
+ #
24
+ # *Result:* -$5
25
+ #
26
+ # *Reasoning:* There are a total of 4 items that are eligible for the
27
+ # promotion. Since that is greater than 3, we can discount the items. The
28
+ # white shirt has a quantity of 1, therefore it will get discounted by
29
+ # +adjustment_amount * 1+ or $5.
30
+ #
31
+ # === Scenario 1-1
32
+ #
33
+ # What about the blue shirt? How much does it get discounted?
34
+ #
35
+ # *Result:* $0
36
+ #
37
+ # *Reasoning:* We have a total quantity of 4. However, we only apply the
38
+ # adjustment to groups of 3. Assuming the white and red shirts have already
39
+ # had their adjustment calculated, that means 3 units have been discounted.
40
+ # Leaving us with a lonely blue shirt that isn't part of a group of 3.
41
+ # Therefore, it does not receive the discount.
42
+ #
43
+ # == Scenario 2
44
+ #
45
+ # User has 4 red shirts in their order. What is the amount?
46
+ #
47
+ # *Result:* -$15
48
+ #
49
+ # *Reasoning:* The total quantity of eligible items is 4, so we the
50
+ # adjustment will be non-zero. However, we only apply it to groups of 3,
51
+ # therefore there is one extra item that is not eligible for the
52
+ # adjustment. +adjustment_amount * 3+ or $15.
53
+ #
54
+ def compute_amount(line_item)
55
+ adjustment_amount = calculator.compute(Item.new(line_item))
56
+ return BigDecimal("0") if adjustment_amount.nil? || adjustment_amount.zero?
57
+
58
+ adjustment_amount = adjustment_amount.abs
59
+
60
+ order = line_item.order
61
+ line_items = applicable_line_items(order)
62
+
63
+ item_units = line_items.sort_by do |applicable_line_item|
64
+ [-applicable_line_item.quantity, applicable_line_item.id]
65
+ end.flat_map do |applicable_line_item|
66
+ Array.new(applicable_line_item.quantity) do
67
+ Item.new(applicable_line_item)
68
+ end
69
+ end
70
+
71
+ item_units_in_groups = item_units.in_groups_of(preferred_group_size, false)
72
+ item_units_in_groups.select! { |group| group.length == preferred_group_size }
73
+ usable_quantity = item_units_in_groups.flatten.count { |item_unit| item_unit.line_item == line_item }
74
+
75
+ amount = adjustment_amount * usable_quantity
76
+ [line_item.discountable_amount, amount].min * -1
77
+ end
78
+
79
+ ##
80
+ # Used specifically for PercentOnLineItem calculator. That calculator uses
81
+ # `line_item.amount`, however we might not necessarily want to discount the
82
+ # entire amount. This class allows us to determine the discount per
83
+ # quantity and then calculate the adjustment amount the way we normally do
84
+ # for flat rate adjustments.
85
+ class Item
86
+ attr_reader :line_item
87
+
88
+ def initialize(line_item)
89
+ @line_item = line_item
90
+ end
91
+
92
+ def discountable_amount
93
+ @line_item.discountable_amount / @line_item.quantity.to_d
94
+ end
95
+ alias_method :amount, :discountable_amount
96
+
97
+ def order
98
+ @line_item.order
99
+ end
100
+
101
+ def currency
102
+ @line_item.currency
103
+ end
104
+ end
105
+ end
106
+ end
107
+ end
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SolidusPromotions
4
+ module Benefits
5
+ class AdjustShipment < Benefit
6
+ include SolidusPromotions::Benefits::ShipmentBenefit
7
+
8
+ def possible_conditions
9
+ super + SolidusPromotions.config.shipment_conditions
10
+ end
11
+ end
12
+ end
13
+ end