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,49 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SolidusPromotions
4
+ module Benefits
5
+ class CreateDiscountedItem < Benefit
6
+ include OrderBenefit
7
+ preference :variant_id, :integer
8
+ preference :quantity, :integer, default: 1
9
+ preference :necessary_quantity, :integer, default: 1
10
+
11
+ def perform(order)
12
+ line_item = find_item(order) || create_item(order)
13
+ set_quantity(line_item, determine_item_quantity(order))
14
+ line_item.current_discounts << discount(line_item)
15
+ end
16
+
17
+ def remove_from(order)
18
+ line_item = find_item(order)
19
+ order.line_items.destroy(line_item)
20
+ end
21
+
22
+ private
23
+
24
+ def find_item(order)
25
+ order.line_items.detect { |line_item| line_item.managed_by_order_benefit == self }
26
+ end
27
+
28
+ def create_item(order)
29
+ order.line_items.create!(quantity: determine_item_quantity(order), variant: variant, managed_by_order_benefit: self)
30
+ end
31
+
32
+ def determine_item_quantity(order)
33
+ # Integer division will floor automatically, which is what we want here:
34
+ # 1 Item, 2 needed: 1 * 1 / 2 => 0
35
+ # 5 items, 2 preferred, 2 needed: 5 / 2 * 2 => 4
36
+ applicable_line_items(order).sum(&:quantity) / preferred_necessary_quantity * preferred_quantity
37
+ end
38
+
39
+ def set_quantity(line_item, quantity)
40
+ line_item.quantity_setter = self
41
+ line_item.quantity = quantity
42
+ end
43
+
44
+ def variant
45
+ Spree::Variant.find(preferred_variant_id)
46
+ end
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_dependency "spree/calculator"
4
+
5
+ # This is a calculator for line item adjustment benefits. It accepts a line item
6
+ # and calculates its weighted adjustment amount based on the value of the
7
+ # preferred amount and the price of the other line items. More expensive line
8
+ # items will receive a greater share of the preferred amount.
9
+ module SolidusPromotions
10
+ module Calculators
11
+ class DistributedAmount < Spree::Calculator
12
+ include PromotionCalculator
13
+
14
+ preference :amount, :decimal, default: 0
15
+ preference :currency, :string, default: -> { Spree::Config[:currency] }
16
+
17
+ def compute_line_item(line_item)
18
+ return 0 unless line_item
19
+ return 0 unless preferred_currency.casecmp(line_item.currency).zero?
20
+
21
+ distributable_line_items = calculable.applicable_line_items(line_item.order)
22
+ return 0 unless line_item.in?(distributable_line_items)
23
+
24
+ DistributedAmountsHandler.new(
25
+ distributable_line_items,
26
+ preferred_amount
27
+ ).amount(line_item)
28
+ end
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_dependency "spree/calculator"
4
+
5
+ module SolidusPromotions
6
+ module Calculators
7
+ class FlatRate < Spree::Calculator
8
+ include PromotionCalculator
9
+
10
+ preference :amount, :decimal, default: 0
11
+ preference :currency, :string, default: -> { Spree::Config[:currency] }
12
+
13
+ def compute(object = nil)
14
+ currency = object.order.currency
15
+ if object && preferred_currency.casecmp(currency).zero?
16
+ preferred_amount
17
+ else
18
+ 0
19
+ end
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_dependency "spree/calculator"
4
+
5
+ module SolidusPromotions
6
+ module Calculators
7
+ class FlexiRate < Spree::Calculator
8
+ include PromotionCalculator
9
+
10
+ preference :first_item, :decimal, default: 0
11
+ preference :additional_item, :decimal, default: 0
12
+ preference :max_items, :integer, default: 0
13
+ preference :currency, :string, default: -> { Spree::Config[:currency] }
14
+
15
+ def compute(object)
16
+ items_count = object.quantity
17
+ items_count = [items_count, preferred_max_items].min unless preferred_max_items.zero?
18
+
19
+ return BigDecimal("0") if items_count == 0
20
+
21
+ additional_items_count = items_count - 1
22
+ preferred_first_item + preferred_additional_item * additional_items_count
23
+ end
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_dependency "spree/calculator"
4
+
5
+ module SolidusPromotions
6
+ module Calculators
7
+ class Percent < Spree::Calculator
8
+ include PromotionCalculator
9
+
10
+ preference :percent, :decimal, default: 0
11
+
12
+ def compute(object)
13
+ preferred_currency = object.order.currency
14
+ currency_exponent = ::Money::Currency.find(preferred_currency).exponent
15
+ (object.discountable_amount * preferred_percent / 100).round(currency_exponent)
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,58 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_dependency "spree/calculator"
4
+
5
+ module SolidusPromotions
6
+ module Calculators
7
+ class TieredFlatRate < Spree::Calculator
8
+ include PromotionCalculator
9
+
10
+ preference :base_amount, :decimal, default: 0
11
+ preference :tiers, :hash, default: { 10 => 10 }
12
+ preference :currency, :string, default: -> { Spree::Config[:currency] }
13
+
14
+ before_validation do
15
+ # Convert tier values to decimals. Strings don't do us much good.
16
+ if preferred_tiers.is_a?(Hash)
17
+ self.preferred_tiers = preferred_tiers.map do |key, value|
18
+ [cast_to_d(key.to_s), cast_to_d(value.to_s)]
19
+ end.to_h
20
+ end
21
+ end
22
+
23
+ validate :preferred_tiers_content
24
+
25
+ def compute_item(object)
26
+ _base, amount = preferred_tiers.sort.reverse.detect do |value, _|
27
+ object.discountable_amount >= value
28
+ end
29
+
30
+ if preferred_currency.casecmp(object.currency).zero?
31
+ amount || preferred_base_amount
32
+ else
33
+ 0
34
+ end
35
+ end
36
+ alias_method :compute_shipment, :compute_item
37
+ alias_method :compute_line_item, :compute_item
38
+
39
+ private
40
+
41
+ def cast_to_d(value)
42
+ value.to_s.to_d
43
+ rescue ArgumentError
44
+ BigDecimal("0")
45
+ end
46
+
47
+ def preferred_tiers_content
48
+ if preferred_tiers.is_a? Hash
49
+ unless preferred_tiers.keys.all? { |key| key.is_a?(Numeric) && key > 0 }
50
+ errors.add(:base, :keys_should_be_positive_number)
51
+ end
52
+ else
53
+ errors.add(:preferred_tiers, :should_be_hash)
54
+ end
55
+ end
56
+ end
57
+ end
58
+ end
@@ -0,0 +1,68 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_dependency "spree/calculator"
4
+
5
+ module SolidusPromotions
6
+ module Calculators
7
+ class TieredPercent < Spree::Calculator
8
+ include PromotionCalculator
9
+
10
+ preference :base_percent, :decimal, default: 0
11
+ preference :tiers, :hash, default: { 50 => 5 }
12
+ preference :currency, :string, default: -> { Spree::Config[:currency] }
13
+
14
+ before_validation do
15
+ # Convert tier values to decimals. Strings don't do us much good.
16
+ if preferred_tiers.is_a?(Hash)
17
+ self.preferred_tiers = preferred_tiers.map do |key, value|
18
+ [cast_to_d(key.to_s), cast_to_d(value.to_s)]
19
+ end.to_h
20
+ end
21
+ end
22
+
23
+ validates :preferred_base_percent, numericality: {
24
+ greater_than_or_equal_to: 0,
25
+ less_than_or_equal_to: 100
26
+ }
27
+ validate :preferred_tiers_content
28
+
29
+ def compute_item(object)
30
+ order = object.order
31
+
32
+ _base, percent = preferred_tiers.sort.reverse.detect do |value, _|
33
+ order.item_total >= value
34
+ end
35
+
36
+ if preferred_currency.casecmp(order.currency).zero?
37
+ currency_exponent = ::Money::Currency.find(preferred_currency).exponent
38
+ (object.amount * (percent || preferred_base_percent) / 100).round(currency_exponent)
39
+ else
40
+ 0
41
+ end
42
+ end
43
+ alias_method :compute_shipment, :compute_item
44
+ alias_method :compute_line_item, :compute_item
45
+
46
+ private
47
+
48
+ def cast_to_d(value)
49
+ value.to_s.to_d
50
+ rescue ArgumentError
51
+ BigDecimal("0")
52
+ end
53
+
54
+ def preferred_tiers_content
55
+ if preferred_tiers.is_a? Hash
56
+ unless preferred_tiers.keys.all? { |key| key.is_a?(Numeric) && key > 0 }
57
+ errors.add(:base, :keys_should_be_positive_number)
58
+ end
59
+ unless preferred_tiers.values.all? { |key| key.is_a?(Numeric) && key >= 0 && key <= 100 }
60
+ errors.add(:base, :values_should_be_percent)
61
+ end
62
+ else
63
+ errors.add(:preferred_tiers, :should_be_hash)
64
+ end
65
+ end
66
+ end
67
+ end
68
+ end
@@ -0,0 +1,40 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_dependency "spree/calculator"
4
+
5
+ module SolidusPromotions
6
+ module Calculators
7
+ class TieredPercentOnEligibleItemQuantity < SolidusPromotions::Calculators::TieredPercent
8
+ preference :tiers, :hash, default: { 10 => 5 }
9
+
10
+ before_validation do
11
+ # Convert tier values to decimals. Strings don't do us much good.
12
+ if preferred_tiers.is_a?(Hash)
13
+ self.preferred_tiers = preferred_tiers.map do |key, value|
14
+ [key.to_i, cast_to_d(value.to_s)]
15
+ end.to_h
16
+ end
17
+ end
18
+
19
+ def compute_line_item(line_item)
20
+ order = line_item.order
21
+
22
+ _base, percent = preferred_tiers.sort.reverse.detect do |value, _|
23
+ eligible_line_items_quantity_total(order) >= value
24
+ end
25
+ if preferred_currency.casecmp(order.currency).zero?
26
+ currency_exponent = ::Money::Currency.find(preferred_currency).exponent
27
+ (line_item.discountable_amount * (percent || preferred_base_percent) / 100).round(currency_exponent)
28
+ else
29
+ 0
30
+ end
31
+ end
32
+
33
+ private
34
+
35
+ def eligible_line_items_quantity_total(order)
36
+ calculable.applicable_line_items(order).sum(&:quantity)
37
+ end
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,61 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "spree/preferences/persistable"
4
+
5
+ module SolidusPromotions
6
+ class Condition < Spree::Base
7
+ include Spree::Preferences::Persistable
8
+
9
+ belongs_to :benefit, class_name: "SolidusPromotions::Benefit", inverse_of: :conditions, optional: true
10
+ has_one :promotion, through: :benefit
11
+
12
+ scope :of_type, ->(type) { where(type: type) }
13
+
14
+ validate :unique_per_benefit, on: :create
15
+ validate :possible_condition_for_benefit, if: -> { benefit.present? }
16
+
17
+ def preload_relations
18
+ []
19
+ end
20
+
21
+ def applicable?(_promotable)
22
+ raise NotImplementedError, "applicable? should be implemented in a sub-class of SolidusPromotions::Rule"
23
+ end
24
+
25
+ def eligible?(_promotable, _options = {})
26
+ raise NotImplementedError, "eligible? should be implemented in a sub-class of SolidusPromotions::Rule"
27
+ end
28
+
29
+ def level
30
+ raise NotImplementedError, "level should be implemented in a sub-class of SolidusPromotions::Rule"
31
+ end
32
+
33
+ def eligibility_errors
34
+ @eligibility_errors ||= ActiveModel::Errors.new(self)
35
+ end
36
+
37
+ def to_partial_path
38
+ "solidus_promotions/admin/condition_fields/#{model_name.element}"
39
+ end
40
+
41
+ def updateable?
42
+ preferences.any?
43
+ end
44
+
45
+ private
46
+
47
+ def unique_per_benefit
48
+ return unless self.class.exists?(benefit_id: benefit_id, type: self.class.name)
49
+
50
+ errors.add(:benefit, :already_contains_condition_type)
51
+ end
52
+
53
+ def possible_condition_for_benefit
54
+ benefit.possible_conditions.include?(self.class) || errors.add(:type, :invalid_condition_type)
55
+ end
56
+
57
+ def eligibility_error_message(key, options = {})
58
+ I18n.t(key, scope: [:solidus_promotions, :eligibility_errors, self.class.name.underscore], **options)
59
+ end
60
+ end
61
+ end
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SolidusPromotions
4
+ class ConditionProduct < Spree::Base
5
+ belongs_to :condition, class_name: "SolidusPromotions::Condition", optional: true
6
+ belongs_to :product, class_name: "Spree::Product", optional: true
7
+ end
8
+ end
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SolidusPromotions
4
+ class ConditionStore < Spree::Base
5
+ belongs_to :condition, class_name: "SolidusPromotions::Condition", optional: true
6
+ belongs_to :store, class_name: "Spree::Store", optional: true
7
+ end
8
+ end
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SolidusPromotions
4
+ class ConditionTaxon < Spree::Base
5
+ belongs_to :condition, class_name: "SolidusPromotions::Condition", optional: true
6
+ belongs_to :taxon, class_name: "Spree::Taxon", optional: true
7
+ end
8
+ end
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SolidusPromotions
4
+ class ConditionUser < Spree::Base
5
+ belongs_to :condition, class_name: "SolidusPromotions::Condition", optional: true
6
+ belongs_to :user, class_name: Spree::UserClassHandle.new, optional: true
7
+ end
8
+ end
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SolidusPromotions
4
+ module Conditions
5
+ # A condition to apply to an order greater than (or greater than or equal to)
6
+ # a specific amount after previous promotions have applied
7
+ #
8
+ # To add extra operators please override `self.operators_map` or any other helper method.
9
+ # To customize the error message you can also override `ineligible_message`.
10
+ class DiscountedItemTotal < ItemTotal
11
+ def to_partial_path
12
+ "solidus_promotions/admin/condition_fields/item_total"
13
+ end
14
+
15
+ private
16
+
17
+ def total_for_order(order)
18
+ order.discountable_item_total
19
+ end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SolidusPromotions
4
+ module Conditions
5
+ class FirstOrder < Condition
6
+ include OrderLevelCondition
7
+ attr_reader :user, :email
8
+
9
+ def eligible?(order, options = {})
10
+ @user = order.try(:user) || options[:user]
11
+ @email = order.email
12
+
13
+ if (user || email) && (completed_orders.present? && completed_orders.first != order)
14
+ eligibility_errors.add(:base, eligibility_error_message(:not_first_order), error_code: :not_first_order)
15
+ end
16
+
17
+ eligibility_errors.empty?
18
+ end
19
+
20
+ private
21
+
22
+ def completed_orders
23
+ user ? user.orders.complete : orders_by_email
24
+ end
25
+
26
+ def orders_by_email
27
+ Spree::Order.where(email: email).complete
28
+ end
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SolidusPromotions
4
+ module Conditions
5
+ class FirstRepeatPurchaseSince < Condition
6
+ include OrderLevelCondition
7
+
8
+ preference :days_ago, :integer, default: 365
9
+ validates :preferred_days_ago, numericality: { only_integer: true, greater_than: 0 }
10
+
11
+ # This is never eligible if the order does not have a user, and that user does not have any previous completed orders.
12
+ #
13
+ # This is eligible if the user's most recently completed order is more than the preferred days ago
14
+ # @param order [Spree::Order]
15
+ def eligible?(order, _options = {})
16
+ return false unless order.user
17
+
18
+ last_order = last_completed_order(order.user)
19
+ return false unless last_order
20
+
21
+ last_order.completed_at < preferred_days_ago.days.ago
22
+ end
23
+
24
+ private
25
+
26
+ def last_completed_order(user)
27
+ user.orders.complete.order(:completed_at).last
28
+ end
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,75 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SolidusPromotions
4
+ module Conditions
5
+ # A condition to apply to an order greater than (or greater than or equal to)
6
+ # a specific amount
7
+ #
8
+ # To add extra operators please override `self.operators_map` or any other helper method.
9
+ # To customize the error message you can also override `ineligible_message`.
10
+ class ItemTotal < Condition
11
+ include OrderLevelCondition
12
+
13
+ preference :amount, :decimal, default: 100.00
14
+ preference :currency, :string, default: -> { Spree::Config[:currency] }
15
+ preference :operator, :string, default: "gt"
16
+
17
+ # The list of allowed operators names mapped to their symbols.
18
+ def self.operators_map
19
+ {
20
+ gte: :>=,
21
+ gt: :>
22
+ }
23
+ end
24
+
25
+ def self.operator_options
26
+ operators_map.map do |name, _method|
27
+ [I18n.t(name, scope: "solidus_promotions.item_total_condition.operators"), name]
28
+ end
29
+ end
30
+
31
+ def eligible?(order, _options = {})
32
+ return false unless order.currency == preferred_currency
33
+
34
+ unless total_for_order(order).send(operator, threshold)
35
+ eligibility_errors.add(:base, ineligible_message, error_code: ineligible_error_code)
36
+ end
37
+
38
+ eligibility_errors.empty?
39
+ end
40
+
41
+ private
42
+
43
+ def operator
44
+ self.class.operators_map.fetch(
45
+ preferred_operator.to_sym,
46
+ preferred_operator_default
47
+ )
48
+ end
49
+
50
+ def total_for_order(order)
51
+ order.item_total
52
+ end
53
+
54
+ def threshold
55
+ BigDecimal(preferred_amount.to_s)
56
+ end
57
+
58
+ def formatted_amount
59
+ Spree::Money.new(preferred_amount, currency: preferred_currency).to_s
60
+ end
61
+
62
+ def ineligible_message
63
+ eligibility_error_message(ineligible_error_code, amount: formatted_amount)
64
+ end
65
+
66
+ def ineligible_error_code
67
+ if preferred_operator == "gte"
68
+ :item_total_less_than
69
+ else
70
+ :item_total_less_than_or_equal
71
+ end
72
+ end
73
+ end
74
+ end
75
+ end
@@ -0,0 +1,37 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SolidusPromotions
4
+ module Conditions
5
+ class LineItemOptionValue < Condition
6
+ include LineItemLevelCondition
7
+
8
+ preference :eligible_values, :hash
9
+
10
+ def eligible?(line_item, _options = {})
11
+ pid = line_item.product.id
12
+ ovids = line_item.variant.option_values.pluck(:id)
13
+
14
+ product_ids.include?(pid) && (value_ids(pid) & ovids).present?
15
+ end
16
+
17
+ def preferred_eligible_values
18
+ values = preferences[:eligible_values] || {}
19
+ values.keys.map(&:to_i).zip(
20
+ values.values.map do |value|
21
+ (value.is_a?(Array) ? value : value.split(",")).map(&:to_i)
22
+ end
23
+ ).to_h
24
+ end
25
+
26
+ private
27
+
28
+ def product_ids
29
+ preferred_eligible_values.keys
30
+ end
31
+
32
+ def value_ids(product_id)
33
+ preferred_eligible_values[product_id]
34
+ end
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,56 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SolidusPromotions
4
+ module Conditions
5
+ # A condition to apply a promotion only to line items with or without a chosen product
6
+ class LineItemProduct < Condition
7
+ include LineItemLevelCondition
8
+
9
+ MATCH_POLICIES = %w[include exclude].freeze
10
+
11
+ has_many :condition_products,
12
+ dependent: :destroy,
13
+ foreign_key: :condition_id,
14
+ class_name: "SolidusPromotions::ConditionProduct"
15
+ has_many :products,
16
+ class_name: "Spree::Product",
17
+ through: :condition_products
18
+
19
+ preference :match_policy, :string, default: MATCH_POLICIES.first
20
+
21
+ def preload_relations
22
+ [:products]
23
+ end
24
+
25
+ def eligible?(line_item, _options = {})
26
+ order_includes_product = product_ids.include?(line_item.variant.product_id)
27
+ success = inverse? ? !order_includes_product : order_includes_product
28
+
29
+ unless success
30
+ message_code = inverse? ? :has_excluded_product : :no_applicable_products
31
+ eligibility_errors.add(
32
+ :base,
33
+ eligibility_error_message(message_code),
34
+ error_code: message_code
35
+ )
36
+ end
37
+
38
+ success
39
+ end
40
+
41
+ def product_ids_string
42
+ product_ids.join(",")
43
+ end
44
+
45
+ def product_ids_string=(product_ids)
46
+ self.product_ids = product_ids.to_s.split(",").map(&:strip)
47
+ end
48
+
49
+ private
50
+
51
+ def inverse?
52
+ preferred_match_policy == "exclude"
53
+ end
54
+ end
55
+ end
56
+ end