Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Initial promotions documentation #2467

Merged
198 changes: 198 additions & 0 deletions guides/promotions/overview.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,198 @@
# Overview of promotions

Solidus's promotions system allows stores to give discounts to customers.

Promotions might be discounts on orders, line items, or shipping charges. The
promotions system provides a set of handlers, rules, and actions that work
together to provide flexible discounts in any scenario.

To account for all of the ways a discount may be applied, the promotions system
has many complex moving parts.

We recommend that you start up a Solidus store on your local machine and see the
built-in promotions functionality yourself, as store administrators have a
flexible promotions system available by default
([`http://localhost:3000/admin/promotions`][promotions-admin]). Here,
administrators can add promotions and create complex strings of promotion rules
and actions if necessary.

[promotions-admin]: http://localhost:3000/admin/promotions

## Promotion architecture

The following sections summarize the main parts of Solidus's promotions system.

<!-- TODO:
Currently there is no documentation about `Spree::PromotionCode`s activating
promotions using a URL.
-->

### Promotion model

The `Spree::Promotion` model defines essential information about a promotion.
This includes the promotion's name, description, and whether the promotion
should be active or not.

Take note of the following promotion attributes:

- `name`: The name for the promotion. This is displayed to customers as an
adjustment label when it is applied.
- `description`: An administrative description for the promotion.
- `usage_limit`: How many times the promotion can be used before becoming
inactive.
- `starts_at` and `expires_at`: Optional date values for the start and end of
the promotion.
- `match_policy`: When set to `all`, all promotion rules must be met in order
for the promotion to be eligible. When set to `any`, just one of the
[promotion rules](#promotion-rules) must be met.
- `path`: If the promotion is activated when the customer visits a URL, this
value is the path for the URL.
- `per_code_usage_limit`: Specifies how many times each code can be used before
it becomes inactive.
- `apply_automatically`: If `true`, the promotion is activated and applied
automatically once all of the [eligibility checks](#eligibility) have passed.

Note that you can access promotion information using the `promotion` method on
its associated `Spree::PromotionRule` and `Spree::PromotionAction` objects:

```ruby
Spree::PromotionAction.find(1).promotion
```

### Promotion handlers

Subclasses of the `Spree::PromotionHandler` model activate a promotion if the
promotion is [eligible](#eligibility) to be applied. There are `Cart`, `Coupon`,
`Page`, and `Shipping` subclasses, each one used for a different promotion
activation method. For more information, see the [Promotion
handlers][promotion-handlers] article.

Once a promotion handler activates a promotion, and all of the eligibility
checks pass, the `Spree::PromotionAction` can be applied to the applicable
shipment, order, or line item.

[promotion-handlers]: promotion-handlers.md

### Promotion rules

The `Spree::PromotionRule` model sets a rule that determines whether a promotion
is eligible to be applied. Promotions may have no rules or many different rules.

By default, `Spree::Promotion`s have a `match_policy` value of `all`, meaning
that all of the promotion rules on a promotion must be met before the promotion
is eligible. However, this can be changed to `any`.

An example of a typical promotion rule would be a minimum order total of $75
USD or that a specific product is in the cart at checkout.

For a list of available rule types and more information, see the
[Promotion rules][promotion-rules] article.

[promotion-rules]: promotion-rules.md

### Promotion actions

The `Spree::PromotionAction` model defines an action that should occur if the
promotion is activated and eligible to be applied. There can be multiple
promotion actions on a promotion.

Typically, a promotion action could be free shipping or a fixed percentage
discount.

A promotion action calculates the discount amount and creates a
`Spree::Adjustment` for the promotion. The adjustment then adjusts the price of
an order, line item, or shipment.

### Promotion adjustments

Finally, the `Spree::Adjustment` model defines the discount amount that is
applied. Each adjustment is created by a `Spree::PromotionAction`.

Every time that the promotion adjustment needs to be recalculated, the
`Spree::PromotionRules` are re-checked to ensure the promotion is still
eligible.

Note that shipments and taxes can also create adjustments. See the adjustments
documentation for more information.

<!-- TODO:
Once merged, link to documentation about adjustments.
-->

## Eligibility

`Spree::Promotion`'s performs a number of checks to determine whether a
promotion is eligible to be applied.

First, it checks that the promotion is active, that its usage limit has not been
reached, that its promotion code usage limit has not be reached, and that all of
the products are promotable products. Finally, it checks the
`Spree::PromotionRule`s.

If all of these checks pass, then the promotion is eligible.

See the `eligible?` method defined in the [Spree::Promotion
model][spree-promotion]:

```ruby
# models/spree/promotion.rb : line 123
def eligible?(promotable, promotion_code: nil)
return false if inactive?
return false if usage_limit_exceeded?
return false if promotion_code && promotion_code.usage_limit_exceeded?
return false if blacklisted?(promotable)
!!eligible_rules(promotable, {})
end
```

Note that promotions without rules are eligible by default.

Once the promotion is confirmed eligible, the promotion can be activated through
the relevant `Spree::PromotionHandler`.

[spree-promotion]: https://github.com/solidusio/solidus/blob/master/core/app/models/spree/promotion.rb

## Promotion flow

This section provides a high-level view of the promotion system in action. For
the sake of this example, the store administrator is creating a free shipping
promotion for orders over $100 USD.

1. The administrator creates a `Spree::Promotion` from the Solidus backend.
- They create a name, description, and optional category for the promotion.
- They choose not to set a usage limit for the promotion.
- They choose not to set a start or end date for the promotion.
- They choose the "Apply to all orders" activation method. Alternatively, they
could have chosen to apply promotions via a promotion code or a URL.
2. The administrator creates `Spree::PromotionRule`s for the promotion.
- In this case, they use the rule type "Item Total"
(`Spree::Promotion::Rules::ItemTotal`) and set the rule so that the
order must be greater than $100 USD.
3. The administrator creates `Spree::PromotionAction`s for the promotion.
- They use promotion action type "Free shipping", which uses the
`Spree::Promotion::Actions::Shipping` model. In this case, the only
available action is "Makes all shipments for the order free".
- Because the promotion action requires a shipment, the
`Spree::PromotionHandler::Shipping` will be used when it is time to activate
the promotion.

Different types of promotions would change the customer's experience of
promotion activation. For example, the customer might be required to enter a
promotion code to activate some promotions, while a another promotion could be
applied automatically.

In this case, because the administrator used the "Apply to all orders"
activation method, the promotion is applied automatically:

1. The customer adds items to their cart. The `Spree::Order` total is greater
than $100 USD.
2. The customer begins the checkout process.
3. The customer enters their shipping information.
4. The `Spree::PromotionHandler::Shipping` handler checks that the
`Spree::PromotionRule`s are met. Because the order total is
greater than $100 USD, the promotion is eligible.
5. The `Spree::PromotionHandler::Shipping` activates the promotion.
6. The `Spree::PromotionAction` associated with the promotion is computed and
applied as a `Spree::Adjustment` that negates the order's shipping charges..
The customer's shipping is now free.
7. The customer completes the checkout process.
87 changes: 87 additions & 0 deletions guides/promotions/promotion-actions.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
# Promotion actions

The `Spree::PromotionAction` model defines an action that should occur if the
promotion is activated and eligible to be applied. There can be multiple
promotion actions on a promotion.

Typically, a promotion action could be free shipping or a fixed percentage
discount.

A promotion action calculates the discount amount and creates a
`Spree::Adjustment` for the promotion. The adjustment then adjusts the price of
an order, line item, or shipment.

With the exception of `Spree::Promotion::Actions::FreeShipping`, promotion
actions have a configurable base calculator. This gives you and store
administrators flexibility for choosing how a promotion amount is calculated.

<!-- TODO:
Once calculator documentation exists, link to it in the above paragraph so
there's more context for anyone wondering what a "base calculator" is in
Solidus.

Similarly, we should link to the adjustments documentation once it's merged.
-->

## Available promotion action types

The following classes are [subclasses of the `Spree::Promotion::Actions`
model][promotion-actions]:

- `CreateAdjustment`: Creates a single adjustment associated to the current
`Spree::Order`.
- `CreateItemAdjustments`: Creates an adjustment for each applicable
`Spree::LineItem` in the current order.
- `CreateQuantityAdjustments`: Creates per-quantity adjustments. For example,
you could create an action that gives customers a discount on each group of
three t-shirts that they order at once.
- `FreeShipping`: Creates an adjustment that negates all shipping charges.

We recommend using `CreateItemAdjustments`s over `CreateAdjustment`. Over-level
adjustments can make calculating accurate refunds and some regions's taxes more
difficult for administrators.

[promotion-actions]: https://github.com/solidusio/solidus/tree/master/core/app/models/spree/promotion/actions

## Eligibility

Note that whenever an order, line item, or shipment with a promotion adjustment
on it is updated, the [eligibility][eligibility] of the promotion is re-checked
and the promotion actions are re-applied.

[eligibility]: overview.md#eligibility

## Register a custom promotion action

You can create a new promotion action for Solidus by creating a new class that
inherits from `Spree::PromotionAction`:
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I might also mention that it must implement the perform(options = {}) method that should return a boolean declaring whether the action was applied successfully.

Also, that it's recommended that it define a remove_from(order) as well.


```ruby
# app/models/spree/promotion/actions/my_promotion_action.rb
module Spree
class Promotion
module Actions
class MyPromotionAction < Spree::PromotionAction
def perform(options={})
...
end

def remove_from(order)
...
end
```

Your promotion action must implement the `perform(options = {})` method. This
method should return a boolean that declares whether the action was applied
successfully. It is also recommend to define a `remove_from(order)` method as
well. See the
[`Spree::Promotion::Actions::CreateItemAdjustments`][create-item-adjustments]
class for an example of these method definitions.

You must then register the custom action in an initializer in your
`config/initializers/` directory:

```ruby
Rails.application.config.spree.promotions.actions << MyPromotionAction
```
[create-item-adjustments]: https://github.com/solidusio/solidus/blob/master/core/app/models/spree/promotion/actions/create_item_adjustments.rb
26 changes: 26 additions & 0 deletions guides/promotions/promotion-handlers.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
# Promotion handlers

The [`Spree::PromotionHandler`][promotion-handler] handles promotion
activation. If the promotion is [eligible][eligibility], then the promotion can
be activated, and finally applied by the `Spree::PromotionAction`s associated
with the promotion.

Promotions can be activated in three different ways using subclasses of the
`Spree::PromotionHandler` model:

- `Cart`: Activates the promotion when a customer adds a product to their cart.
In the Solidus backend, this is the handler used when an administrator assigns
the activation method "Apply to all orders" to a promotion.
- `Coupon`: Activates the promotion when a customer enters a coupon code during
the checkout process.
- `Page`: Activates the promotion when a customer visits a specific store URL.

[promotion-handler]: https://github.com/solidusio/solidus/blob/master/core/app/models/spree/promotion_handler/shipping.rb
[eligibility]: overview.md#eligibility

<!-- TODO:
This article is a stub. If there's no reason to expand it, let's put it back
into the overview.md article.

I can see the coupon and page handlers becoming their own standalone articles.
-->
Loading