bonobos/spree — pull request #57 by gmacdougall (2014-07-22 22:31:50+00:00)
Title: Refactor Line Item Promotion Actions & Eligibility

2-2-dev version of https://github.com/spree/spree/pull/5036

This will help greatly with [% or % off on markdowns](https://trello.com/c/41bTqzrK/228-or-off-of-markdowns) and should also be very helpful in [line item promotions for bundles](https://trello.com/c/vNXk3TXD/191-3-bundle-promotions) and probably more.

- - -

### What is this?

This is a significant refactor to the way that line item promotions are declared eligible for a particular order. The main purpose of this is to remove a couple significant hacks required in order to get the product based line item promotions to function. That functionality is highly desired to be extensible, so I have extracted it to be calculated with the rules class.

This functionality is not used for adjustments which are not line item adjustments.

- - -

### What has changed?

The major components of this are:

`Spree::Promotion::Rules::Rule#actionable?(line_item)`

This code determines if a specific rule can be applied to a particular line item. The goal of this is to be able to extract functionality like this [restriction on products](https://github.com/spree/spree/blob/master/core/app/models/spree/promotion/actions/create_item_adjustments.rb#L31) can be defined in the appropriate rule, rather than in the action. This will allow Spree, and store owners to define more complex logic for eligibility within promotion rules.

`Spree::Promotion#line_item_actionable?(order, line_item)`

This determines if a particular line item can have actions applied to it. It must match either any, or all of the rules (depending on match policy). If true, then the action can be applied to this line item. This returns true by default, and should be changed to restrict what is and is not actionable based upon criteria specific to that rule.

`Spree::Promotion#eligible_rules(promotable, options = {})`

This is a version of `rules_are_eligible?` which returns the eligible rules rather than true/false. `rules_are_eligible?` now uses this internally.

Additionally, there are a couple of changes to the product promotion rule to use this new code for activiation, and to the `create_item_adjustments` action to do similar things.

- - -

### What does this let me do now?

You can now create more advanced restrictions similar to the product based restriction, with logic contained only in the rules class. As shown by the product rule, previously some hacks were required to implement this logic correctly. A few ideas of rules that you can create easily now.

First, define a rule:

```Ruby
class Spree::Promotion::Rules::FancyNewRule < Spree::PromotionRule
def applicable?(promotable)
promotable.is_a?(Spree::Order)
end
end
```

Then define actionable? Some examples:

- Promotion only on variants 1, 2, and 3
```Ruby
def actionable?(line_item)
[1, 2, 3].include? line_item.variant_id
end
```

- Promotion on items that contain the word "Spree"
```Ruby
def actionable?(line_item)
line_item.variant.name.include? "Spree"
end
```

- Promotion on items that are currently $13.37 USD
```Ruby
def actionable?(line_item)
line_item.variant.price_in('USD').amount == '13.37'.to_d
end
```

- - -
### What should eligible? be?

Eligible? determines if I am can apply this promotion to my order. In the case of dealing with line item adjustments, eligible? should be true if any of the line_items are actionable?

As an example:

```Ruby
def eligible?(order, options = {})
order.line_items.any? { |line_item| actionable? line_item }
end
```

You can also apply additional criteria at this level. A great example is the product rule as it exists now:

```Ruby
def eligible?(order, options = {})
return true if eligible_products.empty?
if preferred_match_policy == 'all'
eligible_products.all? {|p| order.products.include?(p) }
elsif preferred_match_policy == 'any'
order.products.any? {|p| eligible_products.include?(p) }
else
order.products.none? {|p| eligible_products.include?(p) }
end
end

def actionable?(line_item)
product_ids.include? line_item.variant.product_id
end
```

Eligible does the complicated logic to decide if we can apply this promotion to a particular order, and actionable? defines if we're allowed to apply the promotion action to a specified line item.