solidus_legacy_promotions 4.4.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/README.md +345 -0
- data/Rakefile +43 -0
- data/app/assets/config/solidus_legacy_promotions/manifest.js +2 -0
- data/app/assets/javascripts/spree/backend/edit_promotion.js +7 -0
- data/app/assets/javascripts/spree/backend/promotions/activation.js +26 -0
- data/app/assets/javascripts/spree/backend/promotions.js +35 -0
- data/app/assets/javascripts/spree/backend/templates/promotions/calculators/fields/tiered_flat_rate.hbs +23 -0
- data/app/assets/javascripts/spree/backend/templates/promotions/calculators/fields/tiered_percent.hbs +23 -0
- data/app/assets/javascripts/spree/backend/templates/promotions/rules/option_values.hbs +12 -0
- data/app/assets/javascripts/spree/backend/views/calculators/tiered.js +54 -0
- data/app/assets/javascripts/spree/backend/views/order/details_adjustments.js +43 -0
- data/app/assets/javascripts/spree/backend/views/promotions/option_values_rule.js +79 -0
- data/app/assets/javascripts/spree/backend/views/promotions.js +3 -0
- data/app/assets/stylesheets/solidus_legacy_promotions/promotions/_edit.scss +138 -0
- data/app/assets/stylesheets/solidus_legacy_promotions/promotions.scss +3 -0
- data/app/assets/stylesheets/spree/backend/sections/_adjustments.scss +3 -0
- data/app/decorators/solidus_legacy_promotions/controllers/solidus_admin/solidus_admin_adjustments_controller_decorator.rb +20 -0
- data/app/decorators/solidus_legacy_promotions/lib/spree_order_state_machine_decorator.rb +18 -0
- data/app/decorators/solidus_legacy_promotions/models/spree_adjustment_decorator.rb +76 -0
- data/app/decorators/solidus_legacy_promotions/models/spree_calculator_returns_default_refund_amount_decorator.rb +13 -0
- data/app/decorators/solidus_legacy_promotions/models/spree_line_item_decorator.rb +11 -0
- data/app/decorators/solidus_legacy_promotions/models/spree_order_decorator.rb +28 -0
- data/app/decorators/solidus_legacy_promotions/models/spree_order_updater_decorator.rb +40 -0
- data/app/decorators/solidus_legacy_promotions/models/spree_product_decorator.rb +16 -0
- data/app/decorators/solidus_legacy_promotions/models/spree_promotion_code_batch_decorator.rb +16 -0
- data/app/decorators/solidus_legacy_promotions/models/spree_shipment_decorator.rb +13 -0
- data/app/helpers/spree/admin/promotions_helper.rb +15 -0
- data/app/helpers/spree/promotion_rules_helper.rb +12 -0
- data/app/jobs/spree/promotion_code_batch_job.rb +26 -0
- data/app/mailers/spree/promotion_code_batch_mailer.rb +15 -0
- data/app/models/spree/calculator/distributed_amount.rb +33 -0
- data/app/models/spree/calculator/flat_percent_item_total.rb +23 -0
- data/app/models/spree/calculator/flexi_rate.rb +22 -0
- data/app/models/spree/calculator/percent_on_line_item.rb +13 -0
- data/app/models/spree/calculator/tiered_flat_rate.rb +52 -0
- data/app/models/spree/calculator/tiered_percent.rb +62 -0
- data/app/models/spree/order_contents.rb +36 -0
- data/app/models/spree/order_promotion.rb +27 -0
- data/app/models/spree/permission_sets/promotion_display.rb +25 -0
- data/app/models/spree/permission_sets/promotion_management.rb +25 -0
- data/app/models/spree/promotion/actions/create_adjustment.rb +81 -0
- data/app/models/spree/promotion/actions/create_item_adjustments.rb +98 -0
- data/app/models/spree/promotion/actions/create_quantity_adjustments.rb +139 -0
- data/app/models/spree/promotion/actions/free_shipping.rb +59 -0
- data/app/models/spree/promotion/order_adjustments_recalculator.rb +92 -0
- data/app/models/spree/promotion/rules/first_order.rb +36 -0
- data/app/models/spree/promotion/rules/first_repeat_purchase_since.rb +36 -0
- data/app/models/spree/promotion/rules/item_total.rb +86 -0
- data/app/models/spree/promotion/rules/minimum_quantity.rb +59 -0
- data/app/models/spree/promotion/rules/nth_order.rb +45 -0
- data/app/models/spree/promotion/rules/one_use_per_user.rb +25 -0
- data/app/models/spree/promotion/rules/option_value.rb +50 -0
- data/app/models/spree/promotion/rules/product.rb +86 -0
- data/app/models/spree/promotion/rules/store.rb +26 -0
- data/app/models/spree/promotion/rules/taxon.rb +91 -0
- data/app/models/spree/promotion/rules/user.rb +34 -0
- data/app/models/spree/promotion/rules/user_logged_in.rb +20 -0
- data/app/models/spree/promotion/rules/user_role.rb +45 -0
- data/app/models/spree/promotion.rb +271 -0
- data/app/models/spree/promotion_action.rb +51 -0
- data/app/models/spree/promotion_advertiser.rb +10 -0
- data/app/models/spree/promotion_category.rb +8 -0
- data/app/models/spree/promotion_chooser.rb +34 -0
- data/app/models/spree/promotion_code/batch_builder.rb +68 -0
- data/app/models/spree/promotion_code.rb +54 -0
- data/app/models/spree/promotion_code_batch.rb +18 -0
- data/app/models/spree/promotion_finder.rb +9 -0
- data/app/models/spree/promotion_handler/cart.rb +75 -0
- data/app/models/spree/promotion_handler/coupon.rb +125 -0
- data/app/models/spree/promotion_handler/page.rb +26 -0
- data/app/models/spree/promotion_handler/shipping.rb +61 -0
- data/app/models/spree/promotion_rule.rb +55 -0
- data/app/models/spree/promotion_rule_store.rb +10 -0
- data/app/models/spree/promotion_rule_taxon.rb +8 -0
- data/app/models/spree/promotion_rule_user.rb +10 -0
- data/app/subscribers/spree/order_promotion_subscriber.rb +20 -0
- data/app/views/spree/order_mailer/cancel_email.html.erb +45 -0
- data/app/views/spree/order_mailer/cancel_email.text.erb +16 -0
- data/app/views/spree/order_mailer/confirm_email.html.erb +84 -0
- data/app/views/spree/order_mailer/confirm_email.text.erb +38 -0
- data/app/views/spree/promotion_code_batch_mailer/promotion_code_batch_errored.text.erb +2 -0
- data/app/views/spree/promotion_code_batch_mailer/promotion_code_batch_finished.text.erb +2 -0
- data/bin/rails +13 -0
- data/config/locales/en.yml +101 -0
- data/config/locales/promotion_categories.en.yml +6 -0
- data/config/locales/promotions.en.yml +6 -0
- data/config/routes.rb +28 -0
- data/db/migrate/20160101010001_solidus_one_four_promotions.rb +126 -0
- data/db/migrate/20161017102621_create_spree_promotion_code_batch.rb +48 -0
- data/db/migrate/20180202190713_create_promotion_rule_stores.rb +14 -0
- data/db/migrate/20180328172631_add_join_characters_to_promotion_code_batch.rb +15 -0
- data/db/migrate/20190106184413_remove_code_from_spree_promotions.rb +46 -0
- data/db/migrate/20220317165036_set_promotions_with_any_policy_to_all_if_possible.rb +20 -0
- data/db/migrate/20230322085416_remove_match_policy_from_spree_promotion.rb +7 -0
- data/db/migrate/20230325132905_remove_unused_columns_from_promotion_rules.rb +10 -0
- data/db/migrate/20231027084517_add_order_promotions_foreign_key.rb +14 -0
- data/db/migrate/20240621100123_add_promotion_code_id_to_spree_adjustments.rb +10 -0
- data/db/migrate/20240622113334_move_adjustment_eligible_to_legacy_promotions.rb +11 -0
- data/lib/components/admin/solidus_admin/orders/show/adjustments/index/source/spree_promotion_action/component.rb +17 -0
- data/lib/components/admin/solidus_admin/promotion_categories/index/component.rb +56 -0
- data/lib/components/admin/solidus_admin/promotions/index/component.rb +104 -0
- data/lib/components/admin/solidus_admin/promotions/index/component.yml +10 -0
- data/lib/components/admin/solidus_legacy_promotions/orders/index/component.rb +15 -0
- data/lib/components/admin/solidus_legacy_promotions/orders/index/component.yml +20 -0
- data/lib/controllers/admin/solidus_admin/promotion_categories_controller.rb +29 -0
- data/lib/controllers/admin/solidus_admin/promotions_controller.rb +46 -0
- data/lib/controllers/backend/spree/admin/promotion_actions_controller.rb +51 -0
- data/lib/controllers/backend/spree/admin/promotion_categories_controller.rb +8 -0
- data/lib/controllers/backend/spree/admin/promotion_code_batches_controller.rb +30 -0
- data/lib/controllers/backend/spree/admin/promotion_codes_controller.rb +48 -0
- data/lib/controllers/backend/spree/admin/promotion_rules_controller.rb +60 -0
- data/lib/controllers/backend/spree/admin/promotions_controller.rb +66 -0
- data/lib/solidus_legacy_promotions/configuration.rb +115 -0
- data/lib/solidus_legacy_promotions/engine.rb +97 -0
- data/lib/solidus_legacy_promotions/migrations/promotions_with_code_handlers.rb +68 -0
- data/lib/solidus_legacy_promotions/testing_support/factories/calculator_factory.rb +7 -0
- data/lib/solidus_legacy_promotions/testing_support/factories/order_factory.rb +22 -0
- data/lib/solidus_legacy_promotions/testing_support/factories/order_promotion_factory.rb +8 -0
- data/lib/solidus_legacy_promotions/testing_support/factories/promotion_category_factory.rb +7 -0
- data/lib/solidus_legacy_promotions/testing_support/factories/promotion_code_factory.rb +8 -0
- data/lib/solidus_legacy_promotions/testing_support/factories/promotion_factory.rb +98 -0
- data/lib/solidus_legacy_promotions/testing_support/factory_bot.rb +28 -0
- data/lib/solidus_legacy_promotions.rb +28 -0
- data/lib/tasks/solidus_legacy_promotions/delete_ineligible_adjustments.rake +8 -0
- data/lib/views/backend/spree/admin/adjustments/_adjustment.html.erb +24 -0
- data/lib/views/backend/spree/admin/orders/_adjustments.html.erb +19 -0
- data/lib/views/backend/spree/admin/orders/_order_details.html.erb +32 -0
- data/lib/views/backend/spree/admin/orders/confirm.html.erb +59 -0
- data/lib/views/backend/spree/admin/promotion_actions/create.js.erb +10 -0
- data/lib/views/backend/spree/admin/promotion_actions/destroy.js.erb +1 -0
- data/lib/views/backend/spree/admin/promotion_categories/_form.html.erb +14 -0
- data/lib/views/backend/spree/admin/promotion_categories/edit.html.erb +10 -0
- data/lib/views/backend/spree/admin/promotion_categories/index.html.erb +47 -0
- data/lib/views/backend/spree/admin/promotion_categories/new.html.erb +10 -0
- data/lib/views/backend/spree/admin/promotion_code_batches/_form_fields.html.erb +22 -0
- data/lib/views/backend/spree/admin/promotion_code_batches/download.csv.ruby +8 -0
- data/lib/views/backend/spree/admin/promotion_code_batches/index.html.erb +65 -0
- data/lib/views/backend/spree/admin/promotion_code_batches/new.html.erb +8 -0
- data/lib/views/backend/spree/admin/promotion_codes/index.csv.ruby +8 -0
- data/lib/views/backend/spree/admin/promotion_codes/index.html.erb +32 -0
- data/lib/views/backend/spree/admin/promotion_codes/new.html.erb +31 -0
- data/lib/views/backend/spree/admin/promotion_rules/create.js.erb +8 -0
- data/lib/views/backend/spree/admin/promotion_rules/destroy.js.erb +3 -0
- data/lib/views/backend/spree/admin/promotions/_actions.html.erb +37 -0
- data/lib/views/backend/spree/admin/promotions/_activations_edit.html.erb +22 -0
- data/lib/views/backend/spree/admin/promotions/_activations_new.html.erb +43 -0
- data/lib/views/backend/spree/admin/promotions/_form.html.erb +67 -0
- data/lib/views/backend/spree/admin/promotions/_promotion_action.html.erb +13 -0
- data/lib/views/backend/spree/admin/promotions/_promotion_rule.html.erb +12 -0
- data/lib/views/backend/spree/admin/promotions/_rules.html.erb +42 -0
- data/lib/views/backend/spree/admin/promotions/actions/_create_adjustment.html.erb +2 -0
- data/lib/views/backend/spree/admin/promotions/actions/_create_item_adjustments.html.erb +6 -0
- data/lib/views/backend/spree/admin/promotions/actions/_create_quantity_adjustments.html.erb +10 -0
- data/lib/views/backend/spree/admin/promotions/actions/_free_shipping.html.erb +0 -0
- data/lib/views/backend/spree/admin/promotions/actions/_promotion_calculators_with_custom_fields.html.erb +29 -0
- data/lib/views/backend/spree/admin/promotions/calculators/_default_fields.html.erb +6 -0
- data/lib/views/backend/spree/admin/promotions/calculators/distributed_amount/_fields.html.erb +56 -0
- data/lib/views/backend/spree/admin/promotions/calculators/flat_rate/_fields.html.erb +6 -0
- data/lib/views/backend/spree/admin/promotions/calculators/tiered_flat_rate/_fields.html.erb +30 -0
- data/lib/views/backend/spree/admin/promotions/calculators/tiered_percent/_fields.html.erb +30 -0
- data/lib/views/backend/spree/admin/promotions/edit.html.erb +40 -0
- data/lib/views/backend/spree/admin/promotions/index.html.erb +124 -0
- data/lib/views/backend/spree/admin/promotions/new.html.erb +14 -0
- data/lib/views/backend/spree/admin/promotions/rules/_first_order.html.erb +0 -0
- data/lib/views/backend/spree/admin/promotions/rules/_first_repeat_purchase_since.html.erb +13 -0
- data/lib/views/backend/spree/admin/promotions/rules/_item_total.html.erb +14 -0
- data/lib/views/backend/spree/admin/promotions/rules/_minimum_quantity.html.erb +5 -0
- data/lib/views/backend/spree/admin/promotions/rules/_nth_order.html.erb +12 -0
- data/lib/views/backend/spree/admin/promotions/rules/_one_use_per_user.html.erb +0 -0
- data/lib/views/backend/spree/admin/promotions/rules/_option_value.html.erb +13 -0
- data/lib/views/backend/spree/admin/promotions/rules/_product.html.erb +15 -0
- data/lib/views/backend/spree/admin/promotions/rules/_store.html.erb +6 -0
- data/lib/views/backend/spree/admin/promotions/rules/_taxon.html.erb +9 -0
- data/lib/views/backend/spree/admin/promotions/rules/_user.html.erb +4 -0
- data/lib/views/backend/spree/admin/promotions/rules/_user_logged_in.html.erb +0 -0
- data/lib/views/backend/spree/admin/promotions/rules/_user_role.html.erb +12 -0
- data/solidus_legacy_promotions.gemspec +29 -0
- metadata +262 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: e9d7efd633c012b29dd74c0586dac9c26b1a39c90e2ba9fb956843913cf00cf5
|
4
|
+
data.tar.gz: 5ebe9bde3952c34717157d2229f34c12e79e95a43114bb459966457fa433a97f
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: f8235d2a505e5c77ed0d1c1867e7fc2296ad1c130b225181225b096deb5f027bfba955fc3ef02824383bb9733956a25badbfd385b6466c478d8fd1947520e487
|
7
|
+
data.tar.gz: 9cd8c70170387dd5c8c2875bcb6a570704410eded75113d7833e816f05948d48acc592de1272661db249add575ae8ac4e0d1b758238e6f1fbae141e8a480c8c2
|
data/README.md
ADDED
@@ -0,0 +1,345 @@
|
|
1
|
+
# Solidus Legacy Promotions
|
2
|
+
|
3
|
+
A Rails Engine that contains the classic Spree/Solidus promotion system, extracted from the other Solidus gems.
|
4
|
+
|
5
|
+
## Installation
|
6
|
+
|
7
|
+
If your Gemfile contains the line `gem "solidus"`, this gem is automatically installed. If you require the individual parts of the Solidus suite, you need to add this gem to your Gemfile:
|
8
|
+
|
9
|
+
```rb
|
10
|
+
gem "solidus_legacy_promotions"
|
11
|
+
```
|
12
|
+
|
13
|
+
This gem is slated for deprecation, as its name implies. We're working on integrating a new implementation for promotions and shipping it later this year.
|
14
|
+
|
15
|
+
## Architecture overview
|
16
|
+
|
17
|
+
Solidus Legacy Promotions ships with a powerful rule-based promotions system that allows you to grant flexible
|
18
|
+
discounts to your customers in many different scenarios. You can apply discounts to the entire
|
19
|
+
order, to a single line item or a set of line items, or to the shipping fees.
|
20
|
+
|
21
|
+
In order to achieve this level of flexibility, the promotions system is composed of four concepts:
|
22
|
+
|
23
|
+
* **Promotion handlers** are responsible for activating a promotion at the right step of the
|
24
|
+
customer experience.
|
25
|
+
* **Promotion rules** are responsible for checking whether an order is eligible for a promotion.
|
26
|
+
* **Promotion actions** are responsible for defining the discount(s) to be applied to eligible
|
27
|
+
orders.
|
28
|
+
* **Adjustments** are responsible for storing discount information. Promotion adjustments are
|
29
|
+
recalculated every time the order is updated, to check if their eligibility persists when the
|
30
|
+
state of the order changes. It is possible to
|
31
|
+
[customize how this recalculation behaves][how-to-use-a-custom-promotion-adjuster].
|
32
|
+
|
33
|
+
> [!NOTE]
|
34
|
+
> Adjustments go beyond promotions and apply to other concepts that modify the order amount.
|
35
|
+
> Taxes are another good example.
|
36
|
+
|
37
|
+
Let's take the example of the following promotion:
|
38
|
+
|
39
|
+
> Apply free shipping on any orders whose total is $100 USD or greater.
|
40
|
+
|
41
|
+
Here's the flow Solidus follows to apply such a promotion:
|
42
|
+
|
43
|
+
1. When the customer enters their shipping information,
|
44
|
+
the [`Shipping`](https://github.com/solidusio/solidus/blob/64b6b6eaf902337983c487cf10dfada8dbfc5160/core/app/models/spree/promotion\_handler/shipping.rb)
|
45
|
+
promotion handler activates the promotion on the order.
|
46
|
+
2. When activated, the promotion will perform
|
47
|
+
some [basic eligibility checks](https://github.com/solidusio/solidus/blob/64b6b6eaf902337983c487cf10dfada8dbfc5160/core/app/models/spree/promotion.rb#L149) (
|
48
|
+
e.g. usage limit, validity dates) and
|
49
|
+
then [ensure the defined promotion rules are met.](https://github.com/solidusio/solidus/blob/64b6b6eaf902337983c487cf10dfada8dbfc5160/core/app/models/spree/promotion.rb#L149)
|
50
|
+
3. When called,
|
51
|
+
the [`ItemTotal`](https://github.com/solidusio/solidus/blob/64b6b6eaf902337983c487cf10dfada8dbfc5160/core/app/models/spree/promotion/rules/item\_total.rb)
|
52
|
+
promotion rule will ensure the order's total is $100 USD or greater.
|
53
|
+
4. Since the order is eligible for the promotion,
|
54
|
+
the [`FreeShipping`](https://github.com/solidusio/solidus/blob/64b6b6eaf902337983c487cf10dfada8dbfc5160/core/app/models/spree/promotion/actions/free\_shipping.rb)
|
55
|
+
action is applied to the order's shipment. The action creates an adjustment that cancels the cost
|
56
|
+
of the shipment.
|
57
|
+
5. The customer gets free shipping!
|
58
|
+
|
59
|
+
This is the architecture at a glance. As you can see, Solidus already ships with some useful
|
60
|
+
handlers, rules, and actions out of the box.
|
61
|
+
|
62
|
+
However, you're not limited to using the stock functionality. In fact, the promotions system shows
|
63
|
+
its full potential when you use it to implement your own logic. In the rest of the guide, we'll use
|
64
|
+
the promotions system to implement the following requirements:
|
65
|
+
|
66
|
+
> We want to uphold a partnership with a new payment platform by offering a 50% shipping discount
|
67
|
+
> when customers pay with it during the checkout.
|
68
|
+
|
69
|
+
In order to do this, we'll have to implement our own handler, rule, and action. Let's get to work!
|
70
|
+
|
71
|
+
## Implementing a new handler
|
72
|
+
|
73
|
+
There's nothing special about promotion handlers: technically, they're just plain old Ruby objects
|
74
|
+
that are created and called in the right places during the checkout flow.
|
75
|
+
|
76
|
+
There is no unified API for promotion handlers, but we can take inspiration from
|
77
|
+
the [existing ones](https://github.com/solidusio/solidus/tree/64b6b6eaf902337983c487cf10dfada8dbfc5160/core/app/models/spree/promotion\_handler)
|
78
|
+
and use a similar format:
|
79
|
+
|
80
|
+
```ruby title="app/models/amazing_store/promotion_handler/payment.rb"
|
81
|
+
# frozen_string_literal: true
|
82
|
+
|
83
|
+
module AmazingStore
|
84
|
+
module PromotionHandler
|
85
|
+
class Payment
|
86
|
+
RULES_TYPE = 'AmazingStore::Promotion::Rules::Payment'
|
87
|
+
|
88
|
+
attr_reader :order
|
89
|
+
|
90
|
+
def initialize(order)
|
91
|
+
@order = order
|
92
|
+
end
|
93
|
+
|
94
|
+
def activate
|
95
|
+
promotions.each do |promotion|
|
96
|
+
promotion.activate(order: order) if promotion.eligible?(order)
|
97
|
+
end
|
98
|
+
end
|
99
|
+
|
100
|
+
private
|
101
|
+
|
102
|
+
def promotions
|
103
|
+
::Spree::Promotion.
|
104
|
+
active.
|
105
|
+
joins(:promotion_rules).
|
106
|
+
where('promotion_rules.type' => RULES_TYPE)
|
107
|
+
end
|
108
|
+
end
|
109
|
+
end
|
110
|
+
end
|
111
|
+
```
|
112
|
+
|
113
|
+
Our promotion handler selects a subset of promotions with a specific rule type that we haven't yet
|
114
|
+
created. Then, it activates the eligible ones, i.e., those who obey its rules.
|
115
|
+
|
116
|
+
Remember that promotion handlers simply apply active promotions to the current order at the correct
|
117
|
+
stage of the order workflow. While other handlers might pick up our promotions, they won't be able
|
118
|
+
to activate it if they run before the payment step. With the new handler, we want to ensure that
|
119
|
+
promotions can be activated after a payment method has been selected for the order.
|
120
|
+
|
121
|
+
Let's call our handler as a callback after the checkout flow has transitioned from the `:payment`
|
122
|
+
state (see
|
123
|
+
the [section on how to customize state machines](state-machines.mdx#customizing-core-behavior)):
|
124
|
+
|
125
|
+
```ruby title="app/overrides/amazing_store/load_payment_promotion_handler.rb"
|
126
|
+
# frozen_string_literal: true
|
127
|
+
|
128
|
+
module AmazingStore
|
129
|
+
module LoadPaymentPromotionHandler
|
130
|
+
def self.prepended(base)
|
131
|
+
base.state_machine.after_transition(from: :payment) do |order|
|
132
|
+
AmazingStore::PromotionHandler::Payment.new(order).activate
|
133
|
+
end
|
134
|
+
end
|
135
|
+
|
136
|
+
::Spree::Order.prepend(self)
|
137
|
+
end
|
138
|
+
end
|
139
|
+
```
|
140
|
+
|
141
|
+
## Implementing a new rule
|
142
|
+
|
143
|
+
Now that we have our handler, let's move on and implement the promotion rule that checks whether the
|
144
|
+
customer is using the promoted payment method.
|
145
|
+
|
146
|
+
We'll allow store admins to edit which payment method carries the discount. The best way to do that
|
147
|
+
is to create a preference for the promotion rule itself:
|
148
|
+
|
149
|
+
```ruby title="app/models/amazing_store/promotion/rules/payment.rb"
|
150
|
+
# frozen_string_literal: true
|
151
|
+
|
152
|
+
module AmazingStore
|
153
|
+
module Promotion
|
154
|
+
module Rules
|
155
|
+
class Payment < ::Spree::PromotionRule
|
156
|
+
DEFAULT_PREFERRED_PAYMENT_TYPE = 'AmazingStore::AmazingPaymentPlatform'
|
157
|
+
|
158
|
+
ALLOWED_PAYMENT_TYPES = [
|
159
|
+
DEFAULT_PREFERRED_PAYMENT_TYPE,
|
160
|
+
'Spree::PaymentMethod::Check',
|
161
|
+
'Spree::PaymentMethod::CreditCard'
|
162
|
+
].freeze
|
163
|
+
|
164
|
+
preference :payment_type, :string, default: DEFAULT_PREFERRED_PAYMENT_TYPE
|
165
|
+
|
166
|
+
validates :preferred_payment_type, inclusion: {
|
167
|
+
in: ALLOWED_PAYMENT_TYPES,
|
168
|
+
allow_blank: true
|
169
|
+
}, on: :update
|
170
|
+
|
171
|
+
def applicable?(promotable)
|
172
|
+
promotable.is_a?(::Spree::Order)
|
173
|
+
end
|
174
|
+
|
175
|
+
def eligible?(order, _options = {})
|
176
|
+
order.payments.any? do |payment|
|
177
|
+
payment.payment_method.type == preferred_payment_type
|
178
|
+
end
|
179
|
+
end
|
180
|
+
end
|
181
|
+
end
|
182
|
+
end
|
183
|
+
end
|
184
|
+
```
|
185
|
+
|
186
|
+
> [!CAUTION]
|
187
|
+
> You may have noticed that we allow the payment type to be blank on creation. This is because
|
188
|
+
> promotion rules are initially created without any of their preferences, so that the correct form can
|
189
|
+
> be presented to the admin when configuring the rule. If we enforced the presence of a payment type
|
190
|
+
> since the very beginning, Solidus wouldn't be able to create the promotion rule and admins would get
|
191
|
+
> an error.
|
192
|
+
|
193
|
+
Now that we have the implementation of our promotion rule, we also need to give admins a nice UI
|
194
|
+
where they can manage the rule and enter the promoted payment type. We just need to create the right
|
195
|
+
partial, where we'll have a local variable `promotion_rule` available to access the current
|
196
|
+
promotion rule instance:
|
197
|
+
|
198
|
+
```markup title="app/views/spree/admin/promotions/rules/_payment.html.erb"
|
199
|
+
<div class="row">
|
200
|
+
<div class="col-6">
|
201
|
+
<div class="field">
|
202
|
+
<%= promotion_rule.class.human_attribute_name(:payment_type) %>
|
203
|
+
</div>
|
204
|
+
</div>
|
205
|
+
<div class="col-6">
|
206
|
+
<div class="field">
|
207
|
+
<%= select_tag "#{param_prefix}[preferred_payment_type]", options_for_select(promotion_rule.class::ALLOWED_PAYMENT_TYPES, promotion_rule.preferred_payment_type), class: 'fullwidth' %>
|
208
|
+
</div>
|
209
|
+
</div>
|
210
|
+
</div>
|
211
|
+
```
|
212
|
+
|
213
|
+
The last step is to register our new promotion rule in an initializer:
|
214
|
+
|
215
|
+
```ruby title="config/initializers/promotions.rb"
|
216
|
+
# ...
|
217
|
+
Rails.application.config.spree.promotions.rules << 'AmazingStore::Promotion::Rules::Payment'
|
218
|
+
```
|
219
|
+
|
220
|
+
When you create a new promotion in the backend, we should now see the _Payment_ promotion rule. For
|
221
|
+
a better experience, we can associate a description so that it's rendered along its form:
|
222
|
+
|
223
|
+
```yaml title="config/locales/en.yml"
|
224
|
+
en:
|
225
|
+
# ...
|
226
|
+
activerecord:
|
227
|
+
attributes:
|
228
|
+
amazing_store/promotion/rules/payment:
|
229
|
+
description: Must use the specified payment method
|
230
|
+
```
|
231
|
+
|
232
|
+
## Implementing a new action
|
233
|
+
|
234
|
+
Finally, let's implement the promotion action that will grant customers a 50% shipping discount. In
|
235
|
+
order to do that, we can take inspiration from the
|
236
|
+
existing [`FreeShipping`](https://github.com/solidusio/solidus/blob/master/core/app/models/spree/promotion/actions/free\_shipping.rb)
|
237
|
+
action:
|
238
|
+
|
239
|
+
```ruby title="app/models/amazing_store/promotion/actions/half_shipping.rb"
|
240
|
+
# frozen_string_literal: true
|
241
|
+
|
242
|
+
module AmazingStore
|
243
|
+
module Promotion
|
244
|
+
module Actions
|
245
|
+
class HalfShipping < ::Spree::PromotionAction
|
246
|
+
# The `perform` method is called when an action is applied to an order or line
|
247
|
+
# item. The payload contains a lot of useful context:
|
248
|
+
# https://github.com/solidusio/solidus/blob/64b6b6eaf902337983c487cf10dfada8dbfc5160/core/app/models/spree/promotion.rb#L129
|
249
|
+
def perform(payload = {})
|
250
|
+
order = payload[:order]
|
251
|
+
promotion_code = payload[:promotion_code]
|
252
|
+
|
253
|
+
results = order.shipments.map do |shipment|
|
254
|
+
# If the shipment has already been discounted by this promotion action,
|
255
|
+
# we skip it.
|
256
|
+
next false if shipment.adjustments.where(source: self).exists?
|
257
|
+
|
258
|
+
# If not, we create an adjustment to apply a 50% discount on the shipment.
|
259
|
+
shipment.adjustments.create!(
|
260
|
+
order: shipment.order,
|
261
|
+
amount: compute_amount(shipment),
|
262
|
+
source: self,
|
263
|
+
promotion_code: promotion_code,
|
264
|
+
label: promotion.name,
|
265
|
+
)
|
266
|
+
|
267
|
+
# We return true here to mark that the shipment has been discounted.
|
268
|
+
true
|
269
|
+
end
|
270
|
+
|
271
|
+
# `perform` needs to return true if any adjustments have been applied by
|
272
|
+
# the promotion action. Otherwise, it should return false.
|
273
|
+
results.any? { |result| result == true }
|
274
|
+
end
|
275
|
+
|
276
|
+
def compute_amount(shipment)
|
277
|
+
shipment.cost * -0.5
|
278
|
+
end
|
279
|
+
|
280
|
+
# The `remove_from` method should undo any actions done by `perform`. It is
|
281
|
+
# used when an order becomes ineligible for a given promotion and the promotion
|
282
|
+
# needs to be removed.
|
283
|
+
def remove_from(order)
|
284
|
+
order.shipments.each do |shipment|
|
285
|
+
shipment.adjustments.each do |adjustment|
|
286
|
+
if adjustment.source == self
|
287
|
+
# Here, we simply remove any adjustments on the order's shipments
|
288
|
+
# created by this promotion action.
|
289
|
+
shipment.adjustments.destroy!(adjustment)
|
290
|
+
end
|
291
|
+
end
|
292
|
+
end
|
293
|
+
end
|
294
|
+
end
|
295
|
+
end
|
296
|
+
end
|
297
|
+
end
|
298
|
+
```
|
299
|
+
|
300
|
+
As you can see, there's quite a bit going on here, but hopefully, the comments help you with the
|
301
|
+
flow of the action and the purpose of the methods we implemented.
|
302
|
+
|
303
|
+
Just like rules, promotion actions can also have preferences and allow admin to define them via the
|
304
|
+
UI. However, in this case, we don't need any of that. Still, Solidus will expect a partial for the
|
305
|
+
action, so we should create an empty ERB file.
|
306
|
+
|
307
|
+
```erb title="app/views/spree/admin/promotions/actions/_half_shipping.html.erb"
|
308
|
+
<!-- Intentionally empty -->
|
309
|
+
```
|
310
|
+
|
311
|
+
> [!TIP]
|
312
|
+
> You can look at
|
313
|
+
? the [`CreateQuantityAdjustments`](https://github.com/solidusio/solidus/blob/64b6b6eaf902337983c487cf10dfada8dbfc5160/core/app/models/spree/promotion/actions/create\_quantity\_adjustments.rb)
|
314
|
+
> action and
|
315
|
+
> the [corresponding view](https://github.com/solidusio/solidus/blob/64b6b6eaf902337983c487cf10dfada8dbfc5160/backend/app/views/spree/admin/promotions/actions/\_create\_quantity\_adjustments.html.erb)
|
316
|
+
for an example of actions with preferences.
|
317
|
+
|
318
|
+
Finally, we need to register our action by adding the following to an initializer:
|
319
|
+
|
320
|
+
```ruby title="config/initializers/promotions.rb"
|
321
|
+
# ...
|
322
|
+
Rails.application.config.spree.promotions.actions << 'AmazingStore::Promotion::Actions::HalfShipping'
|
323
|
+
```
|
324
|
+
|
325
|
+
Like before, let's add a human-friendly description:
|
326
|
+
|
327
|
+
```yaml title="config/locales/en.yml"
|
328
|
+
en:
|
329
|
+
# ...
|
330
|
+
activerecord:
|
331
|
+
attributes:
|
332
|
+
amazing_store/promotion/actions/half_shipping:
|
333
|
+
description: Applies 50% discount in shipping
|
334
|
+
```
|
335
|
+
|
336
|
+
Restart the server and you should now see your new promotion action!
|
337
|
+
|
338
|
+
Let's try it out!
|
339
|
+
|
340
|
+
First of all, go to the _Promotions_ section on the backend and click _New Promotion_. In this case,
|
341
|
+
it makes sense to check the _Apply to all orders_ option, as our promotion doesn't need a code. Once
|
342
|
+
the promotion has been created, add the _Payment_ rule and the _Half shipping_ action.
|
343
|
+
|
344
|
+
You can now go to the frontend and see how the shipment price is dropped by 50% if you select the
|
345
|
+
configured payment method.
|
data/Rakefile
ADDED
@@ -0,0 +1,43 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'rubygems'
|
4
|
+
require 'rake'
|
5
|
+
require 'rake/testtask'
|
6
|
+
require 'rspec/core/rake_task'
|
7
|
+
require 'spree/testing_support/dummy_app/rake_tasks'
|
8
|
+
require 'solidus_admin/testing_support/dummy_app/rake_tasks'
|
9
|
+
require 'bundler/gem_tasks'
|
10
|
+
|
11
|
+
RSpec::Core::RakeTask.new
|
12
|
+
task default: :spec
|
13
|
+
|
14
|
+
DummyApp::RakeTasks.new(
|
15
|
+
gem_root: File.dirname(__FILE__),
|
16
|
+
lib_name: 'solidus_legacy_promotions'
|
17
|
+
)
|
18
|
+
|
19
|
+
require 'yard/rake/yardoc_task'
|
20
|
+
YARD::Rake::YardocTask.new(:yard)
|
21
|
+
# The following workaround can be removed
|
22
|
+
# once https://github.com/lsegal/yard/pull/1457 is merged.
|
23
|
+
task('yard:require') { require 'yard' }
|
24
|
+
task yard: 'yard:require'
|
25
|
+
|
26
|
+
namespace :spec do
|
27
|
+
task :isolated do
|
28
|
+
spec_files = Dir['spec/**/*_spec.rb']
|
29
|
+
failed_specs =
|
30
|
+
spec_files.reject do |file|
|
31
|
+
puts "rspec #{file}"
|
32
|
+
system('rspec', file)
|
33
|
+
end
|
34
|
+
|
35
|
+
if !failed_specs.empty?
|
36
|
+
puts "Failed specs:"
|
37
|
+
puts failed_specs
|
38
|
+
exit 1
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
task test_app: 'db:reset'
|
@@ -0,0 +1,7 @@
|
|
1
|
+
//= require_tree ./templates
|
2
|
+
//= require spree/backend/views/promotions.js
|
3
|
+
//= require spree/backend/views/number_with_currency.js
|
4
|
+
//= require spree/backend/views/promotions/option_values_rule
|
5
|
+
//= require spree/backend/views/calculators/tiered
|
6
|
+
//= require spree/backend/promotions
|
7
|
+
//= require spree/backend/promotions/activation
|
@@ -0,0 +1,26 @@
|
|
1
|
+
Spree.PromotionActivationView = Backbone.View.extend({
|
2
|
+
events: {
|
3
|
+
"change [name=activation_type]": "render"
|
4
|
+
},
|
5
|
+
|
6
|
+
initialize: function(){
|
7
|
+
this.render();
|
8
|
+
},
|
9
|
+
|
10
|
+
render: function(){
|
11
|
+
var activation_type = this.$("[name=activation_type]:checked").val();
|
12
|
+
this.$('[data-activation-type]').each(function(){
|
13
|
+
var selected = ($(this).data('activation-type') === activation_type);
|
14
|
+
$(this).find(':input').prop("disabled", !selected);
|
15
|
+
$(this).toggle(selected);
|
16
|
+
});
|
17
|
+
}
|
18
|
+
});
|
19
|
+
|
20
|
+
Spree.ready(function(){
|
21
|
+
if($("#js_promotion_activation").length) {
|
22
|
+
new Spree.PromotionActivationView({
|
23
|
+
el: $("#js_promotion_activation")
|
24
|
+
});
|
25
|
+
}
|
26
|
+
});
|
@@ -0,0 +1,35 @@
|
|
1
|
+
// This is called on page load and via an ajax request in
|
2
|
+
// app/views/spree/admin/promotion_actions/create.js.erb
|
3
|
+
window.initPromotionActions = function() {
|
4
|
+
$('#promotion-filters').find('.variant_autocomplete').variantAutocomplete();
|
5
|
+
|
6
|
+
$('.promo-rule-option-values').each(function() {
|
7
|
+
if (!$(this).data('has-view')) {
|
8
|
+
$(this).data('has-view', true);
|
9
|
+
new Spree.Views.Promotions.OptionValuesRule({
|
10
|
+
el: this
|
11
|
+
});
|
12
|
+
}
|
13
|
+
});
|
14
|
+
|
15
|
+
$('.js-tiered-calculator').each(function() {
|
16
|
+
if (!$(this).data('has-view')) {
|
17
|
+
$(this).data('has-view', true);
|
18
|
+
new Spree.Views.Calculators.Tiered({
|
19
|
+
el: this
|
20
|
+
});
|
21
|
+
}
|
22
|
+
});
|
23
|
+
};
|
24
|
+
|
25
|
+
Spree.ready(function() {
|
26
|
+
// Add classes to boxes when hovering over delete
|
27
|
+
$('#promotion-filters').on('mouseover', 'a.delete', function(event) {
|
28
|
+
$(this).parent().addClass('action-remove');
|
29
|
+
});
|
30
|
+
$('#promotion-filters').on('mouseout', 'a.delete', function(event) {
|
31
|
+
$(this).parent().removeClass('action-remove');
|
32
|
+
});
|
33
|
+
|
34
|
+
window.initPromotionActions();
|
35
|
+
});
|
@@ -0,0 +1,23 @@
|
|
1
|
+
<div class="fullwidth tier">
|
2
|
+
<a class="fa fa-trash remove js-remove-tier"></a>
|
3
|
+
<div class="row">
|
4
|
+
<div class="col-6">
|
5
|
+
<div class="input-group">
|
6
|
+
<div class="input-group-prepend">
|
7
|
+
<span class="input-group-text">$</span>
|
8
|
+
</div>
|
9
|
+
<input class="js-base-input form-control" type="text" value={{baseField.value}}>
|
10
|
+
</div>
|
11
|
+
</div>
|
12
|
+
<div class="col-6">
|
13
|
+
<div class="input-group">
|
14
|
+
<div class="input-group-prepend">
|
15
|
+
<span class="input-group-text">$</span>
|
16
|
+
</div>
|
17
|
+
<input class="js-value-input form-control"
|
18
|
+
name="{{valueField.name}}" type="text" value={{valueField.value}}>
|
19
|
+
</div>
|
20
|
+
</div>
|
21
|
+
</div>
|
22
|
+
<div class="clear"></div>
|
23
|
+
</div>
|
data/app/assets/javascripts/spree/backend/templates/promotions/calculators/fields/tiered_percent.hbs
ADDED
@@ -0,0 +1,23 @@
|
|
1
|
+
<div class="fullwidth tier">
|
2
|
+
<a class="fa fa-trash remove js-remove-tier"></a>
|
3
|
+
<div class="row">
|
4
|
+
<div class="col-6">
|
5
|
+
<div class="input-group">
|
6
|
+
<div class="input-group-prepend">
|
7
|
+
<span class="input-group-text">$</span>
|
8
|
+
</div>
|
9
|
+
<input class="js-base-input form-control" type="text" value={{baseField.value}}>
|
10
|
+
</div>
|
11
|
+
</div>
|
12
|
+
<div class="col-6">
|
13
|
+
<div class="input-group">
|
14
|
+
<input class="js-value-input form-control right-align"
|
15
|
+
name="{{valueField.name}}" type="text" value={{valueField.value}}>
|
16
|
+
<div class="input-group-append">
|
17
|
+
<span class="input-group-text">%</span>
|
18
|
+
</div>
|
19
|
+
</div>
|
20
|
+
</div>
|
21
|
+
</div>
|
22
|
+
<div class="clear"></div>
|
23
|
+
</div>
|
@@ -0,0 +1,12 @@
|
|
1
|
+
<div class="fullwidth promo-rule-option-value">
|
2
|
+
<div class="row">
|
3
|
+
<div class="col-6">
|
4
|
+
<input class="js-promo-rule-option-value-product-select fullwidth" type="hidden" value="{{ productSelect.value }}">
|
5
|
+
</div>
|
6
|
+
<div class="col-5">
|
7
|
+
<input class="js-promo-rule-option-value-option-values-select fullwidth" name="{{ paramPrefix }}[preferred_eligible_values][{{ productSelect.value }}]" type="hidden" value={{optionValuesSelect.value}}>
|
8
|
+
</div>
|
9
|
+
</div>
|
10
|
+
<a class="fa fa-trash remove js-remove-promo-rule-option-value"></a>
|
11
|
+
<div class="clear"></div>
|
12
|
+
</div>
|
@@ -0,0 +1,54 @@
|
|
1
|
+
Spree.Views.Calculators.Tiered = Backbone.View.extend({
|
2
|
+
initialize: function() {
|
3
|
+
this.calculatorName = this.$('.js-tiers').data('calculator');
|
4
|
+
this.tierFieldsTemplate = HandlebarsTemplates["promotions/calculators/fields/" + this.calculatorName];
|
5
|
+
this.originalTiers = this.$('.js-tiers').data('original-tiers');
|
6
|
+
this.formPrefix = this.$('.js-tiers').data('form-prefix');
|
7
|
+
|
8
|
+
for (var base in this.originalTiers) {
|
9
|
+
var value = this.originalTiers[base];
|
10
|
+
this.$('.js-tiers').append(
|
11
|
+
this.tierFieldsTemplate({
|
12
|
+
baseField: {
|
13
|
+
value: base
|
14
|
+
},
|
15
|
+
valueField: {
|
16
|
+
name: this.tierInputName(base),
|
17
|
+
value: value
|
18
|
+
}
|
19
|
+
})
|
20
|
+
);
|
21
|
+
}
|
22
|
+
},
|
23
|
+
|
24
|
+
events: {
|
25
|
+
'click .js-add-tier': 'onAdd',
|
26
|
+
'click .js-remove-tier': 'onRemove',
|
27
|
+
'change .js-base-input': 'onChange'
|
28
|
+
},
|
29
|
+
|
30
|
+
tierInputName: function(base) {
|
31
|
+
return this.formPrefix + "[calculator_attributes][preferred_tiers][" + base + "]";
|
32
|
+
},
|
33
|
+
|
34
|
+
onAdd: function(event) {
|
35
|
+
event.preventDefault();
|
36
|
+
this.$('.js-tiers').append(
|
37
|
+
this.tierFieldsTemplate({
|
38
|
+
valueField: {
|
39
|
+
name: null
|
40
|
+
}
|
41
|
+
})
|
42
|
+
);
|
43
|
+
},
|
44
|
+
|
45
|
+
onRemove: function(event) {
|
46
|
+
event.preventDefault();
|
47
|
+
$(event.target).parents('.tier').remove();
|
48
|
+
},
|
49
|
+
|
50
|
+
onChange: function(event) {
|
51
|
+
var valueInput = $(event.target).parents('.tier').find('.js-value-input');
|
52
|
+
valueInput.attr('name', this.tierInputName($(event.target).val()));
|
53
|
+
}
|
54
|
+
});
|
@@ -0,0 +1,43 @@
|
|
1
|
+
Spree.Views.Order.DetailsAdjustments = Backbone.View.extend({
|
2
|
+
initialize: function() {
|
3
|
+
this.listenTo(this.model, "change", this.render);
|
4
|
+
this.render()
|
5
|
+
},
|
6
|
+
|
7
|
+
adjustmentTotals: function() {
|
8
|
+
var totals = {};
|
9
|
+
|
10
|
+
var collection = this.collection ? this.collection.chain() : _.chain([this.model]);
|
11
|
+
collection
|
12
|
+
.map(function(item) {
|
13
|
+
return (item.get("adjustments") || [])
|
14
|
+
.filter(function(adjustment) { return (adjustment.eligible === true); });
|
15
|
+
})
|
16
|
+
.flatten(true)
|
17
|
+
.each(function(adjustment){
|
18
|
+
var label = adjustment.label;
|
19
|
+
|
20
|
+
/* Fixme: because this is done in JS, we only have floating point math */
|
21
|
+
totals[label] = (totals[label] || 0);
|
22
|
+
totals[label] += Number(adjustment.amount);
|
23
|
+
});
|
24
|
+
return totals;
|
25
|
+
},
|
26
|
+
|
27
|
+
render: function() {
|
28
|
+
var model = this.model;
|
29
|
+
var tbody = this.$('tbody');
|
30
|
+
var adjustmentTotals = this.adjustmentTotals()
|
31
|
+
|
32
|
+
tbody.empty();
|
33
|
+
_.each(adjustmentTotals, function(amount, label) {
|
34
|
+
var html = HandlebarsTemplates["orders/details_adjustment_row"]({
|
35
|
+
label: label,
|
36
|
+
amount: Spree.formatMoney(amount, model.get("currency"))
|
37
|
+
});
|
38
|
+
tbody.append(html);
|
39
|
+
});
|
40
|
+
|
41
|
+
this.$el.toggleClass("hidden", _.isEmpty(adjustmentTotals));
|
42
|
+
}
|
43
|
+
})
|