Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with
or
.
Download ZIP

Loading…

Pricing & promotions architecture #41

Open
jedateach opened this Issue · 20 comments

3 participants

@jedateach
Owner

The shop module is reaching a point where a single price is not enough. Pricing is such a core aspect of eCommerce systems. To get this done right will greatly merit the shop module.

Ideally the pricing system for shop should:

  • Allow easy enter of pricing values - via UI, file upload, or API connection.
  • Allow automated price changes.
  • Allow custom calculations.
  • Be flexible as to allow a range of different types of price manipulations, in different configurations and orders.
  • Prevent tampering, eg selling a product for $0 because of some accident

Here's a range of the kinds of price calculations and contributing values:
shop price calculation
original google doc illustration

Other related things to display to the user:

  • Price fluctuations - Display history graph of price changes.
  • Name your price - There may be some circumstances where the customer chooses what to pay.
  • Total savings - Original price, minus discounted price.

Product

  • Cost / Wholesale - the amount paid by the merchant to obtain the goods.
  • Recommended Retail - price that manufacturer recommends.
  • Original - cost + whatever markup merchant has decided. This is what "Price" is currently.
  • Price Adjustments - addition or reduction of original price.
    • Temporary Reduction - time based discount.
    • Tiered / Quantity - different prices for different quantities.
    • Group - specific member groups pay different amounts.
    • Location - charge differently per region.
  • Tax inclusive / exclusive - tax for this product. Item tax can vary in some countries.
  • Currency converted - convert into user-selected currency

Out of this pricing, the user sees some final price, and they click 'add to cart'. This could be called the "Selling Price".

Templates will need to be updated, or provided in documentation on how to display price aspects.

Cart Item(s)

Custom calculations could be performed for items, or per line changes might be applicable.

  • Coupon/voucher/gift discount

If the product catalog is updated, then order items also need to be updated.

Order

  • Items Subtotal
  • Discount(s)
  • Tax
  • Shipping + handling
  • Grand total

Some could be totals of item values, or unique values of their own.

Questions to Answer

  • What to keep track of? Should I record the fact that an order item was added with a temporary discount, tiered pricing, group pricing, and used a voucher, and has tax X...or just a single price, and hope that if you ever need to, you can work out those things from some separate history?
  • What order should calculations be done? Prestashop gives the option to choose order.
  • How should/can I allow developers to customise or create price calculations?
  • Do I need to introduce some kind of rules system, like Magento allows?
  • Should I aim for configuration or stick to some convention? What should be: core, module, developer-customisable, merchant-customisable?
  • How do discounts get overridden or appended?
@jedateach
Owner

Price recalculations could be triggered when the product version no longer matches. Alternatively, the last modified date could be stored in the OrderItem, and a check is done against that on every request. It it differs, then recalculate price, and notify the customer.

@jedateach
Owner

An issue I'm facing is around the separation of raw values with template viewable fields.
They can't be the same thing, because you can't do calculations with DBFields...can you?

A good solution would be creating a function like getSellPrice, which outputs a raw value, but it's also casted via the $casting array. However, you can't cast to Money, because that needs to take both an amount and a currency.

Options:

  • Rename Price to 'BasePrice' to store actual prices in
  • Create a 'SellPrice' for displaying modified prices on template

I don't really want to force people to update their templates, but that might be the only way.

@jedateach
Owner

Displaying prices could be made easier with some abstracted toolset.

eg:

return ShopTools::displayValue($amount);

Returns a Money object with the passed value, and current site currency, and price is formatted according to current locale.

@jedateach
Owner

Need to avoid a system of price adjustments via extension hooks. We can't confirm the order these hooks are called.

@jedateach
Owner

I've renamed $product->Price to $product->BasePrice, and now introduced a function $product->sellingPrice() , which will eventually include discount calculations etc, but just returns BasePrice for now.
Need to update migration task.

I also added in CostPrice for revenue calculations.

@jedateach jedateach referenced this issue from a commit
@jedateach jedateach ENHANCEMENT: improved pricing system to allow price alterations befor…
…e template usage.

Renamed db field 'Price' to 'BasePrice', and introduced sellingPrice() function and getPrice() function. The get price function just returns the sellingPrice, which is the place to make modifications to the BasePrice before presenting to the user. This could be a temporary product discount, or member/region specific pricing.

CMS field updates also.

relates to #41
dabb81c
@jedateach jedateach was assigned
@jedateach
Owner

I'm working on a diagram of the pricing architecture:
https://docs.google.com/drawings/d/1203XHXeol__qNye3hGL20sWWVeXYPQ0-p_vKJdqcTPM/edit

@jedateach
Owner

OSCommerce has an orderable Order Totals system:
http://www.oscommerce.info/confluence/display/OSCDOC22/Order+Total

@jedateach
Owner

The Oracle ATG Commerce programming guide shows that their system is highly broken down into PricingEngines, PricingCalculators, and more.
http://docs.oracle.com/cd/E26180_01/Platform.94/ATGCommProgGuide/html/s0801usingandextendingpricingservices01.html

@jedateach jedateach referenced this issue in burnbright/silverstripe-shop-discount
Closed

Generic discounting system #5

@jedateach jedateach changed the title from Pricing architecture to Pricing & promotions architecture
@jedateach
Owner

After doing a fair bit of research of other systems, I'm getting a clearer picture of what we can do to make the shop pricing and promotions system more flexible.

There are three areas that prices could be adjusted:

  • Specific prices for products. A product has a base price, which can be adjusted or overridden by looking up specific prices. Each specific price can have a number of associated criteria, such as: geographic region, currency, time period, membership group. Existing solutions: extended pricing zoned pricing
  • Cart price rules - Rules which specify conditions to be met, and perform actions. Criteria could be: customer, time period, minimum amount (incl/excl tax/shipping), number of uses, uses per user, shipping carrier, customer group, and/or product selection. Actions include: discounting shipping, discounging cart/items, or adding discounted items to the cart. Adding a code will make the rule work like a coupon/voucher. Specifying partial use will mean that another voucher is produced if the value of the voucher is greater than the amount used. Existing solutions: discount
  • Catalog price rules - Apply discounting to a range of products in the catalog, specified by some criteria. Criteria are much the same as the above cart rules. (note: I don't yet fully understand how catalog rules are a better alternative to cart rules. It may be that catalog rule discounts can be visually presented when browsing the catalog)

Rules will need priorities to decide which rules beat each other, and perhaps which criteria should be matched before others. eg: Country > Group vs Group > Country
Another consideration is: do you allow stacking of rules? eg: $x off product y, and then a store-wide discount, and a coupon on top of those?

Most of the above has been inspired by prestashop's implementation. sample rules

@jedateach
Owner

One caveat I've realised for the specific prices system is that it would conflict with variations.
Do we say that variations can't have a price if you use specific prices?
Or, should variations instead adjust the base price, rather than replace it?
@nimeso - thoughts?

@nimeso
@markguinn
Owner

I would tend to agree. The "specific prices for products" level needs to be able to apply to both. I think it's worth the extra work. The HasPromotionalPricing extension in my shop-extendedpricing module is basically already implementing that at a very basic level. You might be able to lift some of the logic from that class to apply to both products and variations. It cascades down from categories as well which you may or may not want in this case.

@jedateach
Owner

Thanks for the input guys. We should definitely get this right on paper before implementing. I think I'll split this issue into a few issues. My current focus/need is on getting cart rules sorted.

The way prestashop works, is that variations (attribute combinations) adjust the base price. I assume they would then use a cart rule to make specific product variations get discounted.

@jedateach
Owner

To implement a rules system/engine, it might be worth looking at something like the Ruler library. article about Ruler
This could be wrapped around some DataModels, and adding CMS UI for editing rules.

To help solve the issue of stacking rules: you can define an ordering of all rules. As the rules are evaluated, they can perform multiple actions..eg multiple discounts per product. Some rules could flagged to halt processing if they are reached/matched. This way you can create optional rules stacking.

Other research:
http://www.infoq.com/articles/Rule-Engines
http://www.theregister.co.uk/2006/08/03/rules_engines_part1/
http://en.wikipedia.org/wiki/Business_rules_engine
http://v1.lemonstand.com/docs/shopping_cart_price_rules/
http://v1.lemonstand.com/docs/managing_catalog_level_price_rules/

@markguinn
Owner

I like the idea of using Ruler. It seems solid. The only issue I see is how to persist rules to the database. Sounds like you may already have a plan for that though.

@jedateach
Owner

@markguinn yes, I think this can be approached first as an ss-module, as 'business rules' have many applications.

Possibly harder to implement might be the CMS UI, but I see that someone has implemented something re-usable: https://github.com/chrisjpowers/business-rules

My thoughts about DB representation are that there would be a Rule dataobject, which has many conditions. These conditions might best be represented as serialised json. I'm not sure you'd need to join to rule conditions from anywhere other than a rule.

When it comes to running the currently active rules, the json is unserialised into the Ruler format, and run as normal.

Matching rules run pre-defined actions when conditions are met.

Rules will need to be run for each order item.

The entire active rules set might be able to be cached for speed.

@jedateach
Owner

I have started implementing a SS-generic rules module using Ruler, which I will then adapt to be used for the shop module.
https://github.com/burnbright/silverstripe-rules

@jedateach
Owner

Another thing I've realised is that each rule/discount will have different kinds of actions. These may include things like:

  • discount each cart item unit price by a fixed amount
  • discount each cart item unit price by a percentage of the original price
  • discount the shopping cart subtotal by a percentage of the original subtotal
  • discount the shopping cart subtotal by a fixed amount
  • add discounted item(s) to the order (buy x, get y free)
  • discount shipping/handling costs
  • send voucher to be used in a future order
@jedateach
Owner

I'm removing the 1.0.0 milestone from this issue. I believe it is still important - some has been implemented in discount module, and some has yet to be addressed.

@jedateach jedateach removed this from the 1.0.0 - for SilverStripe 3 milestone
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Something went wrong with that request. Please try again.