-
-
Notifications
You must be signed in to change notification settings - Fork 1.3k
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
jhawthorn
merged 9 commits into
solidusio:master
from
benjaminwil:initial_promotions_documentation
Feb 15, 2018
Merged
Changes from all commits
Commits
Show all changes
9 commits
Select commit
Hold shift + click to select a range
f2ebde5
Port Spree promotions guide
benjaminwil 8e54492
Rewrite article about Spree::PromotionRules
benjaminwil 82b1f8a
Rewrite article about Spree::PromotionActions
benjaminwil d37c05d
Add new article about Spree::PromotionHandlers
benjaminwil 245c011
Add overview of promotion eligibility
benjaminwil 10848df
Document promotion flow for admins and customers
benjaminwil f59b92d
Re-summarize promotion system and Spree::Promotion
benjaminwil 7e767c0
Summarize promotion handlers, actions, and rules
benjaminwil a4c28c9
Make revisions to initial promotions documentation
benjaminwil File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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`: | ||
|
||
```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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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. | ||
--> |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
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.