Skip to content

Instantly share code, notes, and snippets.

@jedateach
Last active December 16, 2015 08:59
Show Gist options
  • Save jedateach/5409607 to your computer and use it in GitHub Desktop.
Save jedateach/5409607 to your computer and use it in GitHub Desktop.
Shop rewrite prototyping (created around march 2012)
<?php
//set modifiers (in config)
OrderFooterLines::setLines(array(
'SubTotal', //shouldn't be included in db - for display purposes only
'Discount', //not always included
'Shipping', //provide different options
'SubTotal', //shouldn't be included in db - for display purposes only
'PaymentFee',
'RandomSurcharge',
'Tax', //could depend on country
'CreditNote', //not always included
));

Cart Upgrade Plan

A cart has may order lines, for individual products. Might need to look at how the grid field works...component based.

What could be included:

  • columns per line - title (subtitle), photo, unit price, (discount) quantity, line total
    • customisable: eg: weight
    • actions: eg: remove, save for later
  • subtotal lines - tax, shipping, discount, account credit. Calculation order is customisable.
  • grand total

Rules System

If built, it should be only available to developers to liase with store owners. Perhaps consider a rules engine: http://www.swindle.net/php-rules/

<?php
/**
* Playing with how one might go about creating an order via Class/Object calls.
*
* @see http://inchoo.net/ecommerce/magento/programmatically-create-order-in-magento/
*
*/
$order = new Order();
//Order status is new
//Created date is stored
//set details of customer
$order->setSetCustomerDetails(array(
'FirstName' => 'Jeremy',
'Surname' => 'Shipman',
'Email' => 'jeremy@burnbright.co.nz',
//'Member' => Member::currentUser()
));
//assign member
$order->setMember(Member::currentUser());
//create and set address
$address = Address::create(array(
'Street' => '9 Menin Road', //could be provided as 'StreetNumber', 'StreetNumberSuffix', 'StreetName', 'StreetType', 'Street Direction'
'Suburb' => 'Raumati Beach', //or 'Village' 'Hamlet'
'City' => 'Kapiti Coast', //
'State' => 'Wellington', //also accepts 'District', 'Province', 'County', or 'Region'
'PostCode' => '5032', //also accepts 'Zip' 'PostalCode'
'Country' => 'New Zealand'
));
$order->setShippingAddress($address);
$order->setBillingAddress($member->getAddress());
//add products
$itema = $order->add($producta);
$itemb = $order->add($productb,4);
$itemc = $order->add($productd,1,array(
"Length" => 15.4,
"Colour" => "brown"
));
if(!($itema && $itemb && $itemc)){
//do something if any item isn't available
//perhaps notify the customer asking if they'd like to proceed
}
$order->setShippingMethod($shipping);
$itemb->setDiscount(0.3); //30% discount on itemb
$order->setDiscount(0.2); //20% discount on entire order
$order->setPaymentMethod();
$order->place(); //workout and store totals, show up in customer's account page
if($order->canProcess()) //do we have everything required to move to processing
$order->process();//takes next step in order fulfilment process
//this could then send request to the customer for payment
$order->cancel();

Modifiers Upgrade Plan

The modifier system needs some serious thought. It is difficult to understand how it should be used. The code is not clear and concise. MVC "View" code is coupled with "Model" code. What is possible to do with modifiers is not well understood, documented, or unit tested.

Definition of modifiers: Non-item deductions or additions that affect the order total.

Here is a breakdown of possible types of lines that would show in an order:

  • OrderAttribute (Lines)
    • Items * Product * Digital Download * Custom Product * Free Gift
      • Modifiers
        • Add
          • Shipping
          • Tax
          • Payment Gateway Fees
        • Subtract
          • Coupon Discount
          • Group Discount
          • Credit
      • SubTotal
      • RunningTotal
      • Information
      • Total

Some lines should not be classed as a modifiers, but just an extra piece of displayed information.

Why even use modifiers?

Advantage of having modifiers as their own set/class of lines?

  • common rendering systems
  • scalable - a standardised interface for creating new footer lines

Could these simply be applied to an order as additional fields? eg: ShippingCost, TaxCost, Discount. New additional fields can be added via decorators. Yes, but modifiers will be considered the default solution.

Shipping was once done with ShippingCalculators ...I wonder if that should be adopted once again?

Requirements

This is what I think you should be able to do:

  • define the order in which lines are displayed/calculated eg: items...subtotal->shipping->subtotal->discount->tax...total.

  • ability to include/exclude, eg add/remove a discount, or place an order with no shipping.

  • pick from different sub-types: eg - shipping provider options

    • options can automatically change, based on order information
    • optionally require one be chosen before order is placed, such as shipping
  • chargable (eg shipping), deductable (eg discount), or ignored (eg tax inclusive).

  • display modifier info anywhere on site, eg shipping info, or entered coupon.

  • graceful degredation - historical orders still total up correctly, even if modifiers have been disabled.

    • what about if they have been removed from system? ->not a priority: possibly include migration scripts.
  • display subtotals, or running totals at any point, without having them save to database.

  • individual template/view per line. ie each line need not look the same.

  • recalculate values on cart (or modifiers) modification, and especially final checkout steps...or on every request?

    • database writes should not be preformed on every request, if they are not needed.
  • All order information should be stored in the database, and not require code to do any calculations. It is ok to do calculations before an order is placed, but afterwards all order information should be retrieved from database.

Plan

  • Split out all the view/rendering-related code into a ViewableData class. It should be separate from model code.

To Consider

  • Should orders be completely immutable once they have been placed? > more yes than no
  • Should lines be stored as negative values, or be flagged?
  • Can an order have more than one of the same modifier?...probably not needed in most cases, but it might be useful to allow it anyway. eg: two discount coupons applied to an order.

Problems

When to calculate orders. They need to be calculated after any changes have been made, but not until all changes have been made.

Possible solutions:

  • Overload controller, and add a hook after actions have been called

  • Use a different function for adding Cart to templates, which does a recalculation

  • Run calculation every time order is accessed

  • Calculate only if changes dirty

<?php
class ModifierTest extends FunctionalTest{
function Basics(){
$modifier = new OrderModifier();
}
//check charge, deduct, and ignore calculations
function AddModifier(){
//check that order of modifiers is correct, before place, after place.
}
//make sure historical records don't change - prices, what invoices look like, etc
//changing available modifiers and their order
//add/remove modifiers and check effects
//subtypes
//check that required modifiers are included when order is placed
//what about if they have been removed from system? ->not a priority: possibly include migration scripts.
//display subtotals, or running totals at any point, without having them save to database.
//individual tehttp://marketplace.eclipse.org/marketplace-client-intro?mpc_install=791886mplate/view per line. ie each line need not look the same.
//recalculate values on cart (or modifiers) modification, and especially final checkout steps...or on every request?
//basic title, value checks
//try adding a non-existant modifier
//try add a modifier that already exists
//remove a modifier that can't be removed
//permission checks - who can add modifiers
//plus tests for individual modifiers
}
<?php
class Order extends DataObject{
/**
* Add new item to the order.
*
* @param Buyable $buyable - a dataobject extended by Buyable
* @param int $quantity
* @param array $options
*/
function add(DataObject $purchasable,int $quantity = 1, array $options = null){
$this->extend('onBeforeAdd');
//if can add
//right type
//permission
//else
return false; //TODO: store a message with the reason product couldn't be added.
//find existing OrderItem in order
//or create new OrderItem
$item = $purchasable->createNewItem();
$item->OrderID = $this->ID;
//set price and attributes
$item->setQuantity($quantity);
$item->setOptions($options);
//this will automatically figure out variation and options
//or return false
$item->setPrice($purchasable->getPrice());
$item->calculateAmount();
$item->write();
$this->extend('onAfterAdd');
return $item;
}
}
<?php
/**
* Manage the summary/total lines in an order.
* Some lines will be associcated with db-stored data, and others will be just useful information that displays.
*
* Some lines are required before an order can be completed...eg shipping.
* Some lines may not be included at all...eg discount.
*
* Used wherever an order summary is produced.
*
* @todo: work out mechanism for creating, finding or ignoring a modifier.
* @todo: design tests
*
*/
class OrderFooterLines{
static $registered_lines = array(); //configuration of where lines should show up, and what can be included
protected $producedlines = array();
protected $cart = null;
function __construct(){
$this->cart = ShoppingCart::current_order();
}
/**
* Creates all the footer lines for the current order,
* with appropriate running totals.
*
* @return array $productlines
*/
function getLines(){ //should this be in order?
$itemssubotal = $this->cart->SubTotal();
//get items total
$runningtotal = $itemssubtotal;
$subtotal = 0;
foreach($registered_lines as $regline){
//check if line is an actual class, throw error if not
//create or find
if($line = $this->getModifier($regline)){
$runningtotal = $line->apply($runningtotal);
//allow multiple of the same modifier - eg two discount vouchers?
$producedlines[] = $line;
}
//if subtotal displayed, then reset subtotal to 0
}
return $producedlines;
}
/**
* Mechanism for retrieving existing modifiers
*/
function getModifier($modifier){
if(ClassInfo::exists($modifier)){
$line = new $modifier();
//search for existing
if($line->required()){
return $line;
}
}
return null;
}
/**
* Adds a new modifier to the current order.
*/
function addModifier(OrderModifier $modifier){
}
/**
* Configure the ordering of lines in an order.
* @param array $lines
*/
function setLines(array $lines){
self::$registered_lines = $lines;
}
}
<?php
/**
* Decorator for product that allows adding a temproary discount.
*/
class ProductDiscount extends DataObjectDecorator{
}
#TODO: allow discounting variations - I could imagine people wanting to discount an unpopular size/color.
/**
* A decorator for Product_OrderItem that allows storing a discount value.
*/
class ProductOrderItemDiscount extends DataObjectDecorator{
function extraStatics(){
return array(
'db' => array(
'Discount' => 'Currency'
)
);
}
}
<?php
/**
* ShoppingCart - provides a global way to interface with the cart (current order).
*
* This can be used in other code by calling $cart = ShoppingCart::singleton();
*
*
* This version of shopping cart has been rewritten to:
* - Seperate controller from the cart functions, abstracts out and encapsulates specific functionality.
* - Reduce the excessive use of static variables.
* - Clearly define an API for editing the cart, trying to keep the number of functions to a minimum.
* - Allow easier testing of cart functionality.
* - Message handling done in one place.
* This is not taking a step backward, be cause the old ShoppingCart / Controller seperation had all static variables/functions on ShoppingCart
*
* Note: an alternative might be to provide this functionality on the Order class.
* The shopping cart would then act as a controller interface for modifying the order.
*
*
* @author: Jeremy Shipman, Nicolaas Francken
* @package: ecommerce
*
* @todo support buyable
* @todo country selection
* @todo modifiers
* @todo order item parameters
* @todo produce errors
* @todo handle rendering?
* @todo handle setting quantity
*/
class ShoppingCart extends Object{
protected static $session_variable = "EcommerceShoppingCart"; //used for setting/getting cart things from the session
protected static $current_order = null; //stores a reference to the current order object
protected static function get_order(){
//get or make an order
//store order id in session
}
/**
* This is how you can get the cart from anywhere in code.
* @return ShoppingCart Object
*/
protected static $singletoncart = null;
static function get(){ //could call this 'get' or something else that is short
if(!self::$singletoncart){
self::$singletoncart = new ShoppingCart();
}
return self::$singletoncart;
}
/**
* Adds any number of items to the cart.
* @param $buyable - the buyable (generally a product) being added to the cart.
* @param $quantity - number of items add.
* @param $parameters - array of parameters to target a specific order item. eg: group=1, length=5
* @return the new item or null
*/
function addItem($buyable,$quantity = 1, $parameters = null){
//find existing order item or make one
//check that that number can be added
//save to current order
}
/**
* Removes any number of items from the cart.
* @return boolean - successfully removed
*/
function removeItem($buyable,$quantity = 1, $parameters = null){
//check for existence of item
if($quantity == 'all' || $quantity <= 0){
//remove all items with $productid from the current order
}
//otherwise only remove $quantity
}
/**
* Clears the cart contents completely by removing the orderID from session, and thus creating a new cart on next request.
*/
function clear(){
//simply clear the orderid from session
}
/**
* Helper function for making / retrieving order items.
*/
protected function findOrMakeItem($productid,$parameters){
//check for product existence & permission to do stuff
}
/**
* Stores a message that can later be returned via ajax or to $form->sessionMessage();
* @param $message - the message, which could be a notification of successful action, or reason for failure
* @param $type - please use good, bad, warning
*/
protected function message($message, $type = 'good'){
}
/**
* Retrieves all good, bad, and ugly messages that have been produced during the current request.
* @return array of messages
*/
function getMessages(){
}
}
/**
* ShoppingCart_Controller
*
* Handles the modification of a shopping cart via http requests.
* Provides links for making these modifications.
*
* @author: Jeremy Shipman, Nicolaas Francken
* @package: ecommerce
*
* @todo supply links for adding, removing, and clearing cart items
* @todo link for removing modifier(s)
*/
class ShoppingCart_Controller extends Object{
}
Product ID VariationID Title Long Description Price Stock Category Colour Size Weight Photo Variation Variation1 Variation2 Variation3 Variation4 Variation5 Variation6
123 Beach Ball A beach ball with a difference. This ball is made from the finest plastic in the world. Hours of fun are to be had with the whole family down at the beach with this ball. Sports 0.2 beachball.jpg
123 123-1 Beach Ball 5.00 1 Blue 1 Size:1 Colour:Blue
123 123-2 Beach Ball 5.00 3 Red 2 Size:2 Colour:Red
123 123-3 Beach Ball 6.00 3 Blue 2 Size:2 Colour:Blue
124 Socks The comfiest pair of socks you'll ever own. 12.00 12 Apparel Black,White 8,9,10 0.2 socks.png Size:8,9,10 Colour:Black,White
125 100% Cotton T-Shirt Finest t-shirt you'll ever own 25.00 1 Apparel White,Red S,M,L,XL 0.5 tshirt.jpg Gender:Male, Female Size:S,M,L,XL Colour:White,Red
CAR001 Car An actual car 25000.00 15 Automotive 3000 car.jpg
23140402 Mp3 Player Listen to your music on the go with this Mp3 player. Plays Mp3 files to your headphones. Electronics 0.6 mp3player.jpg
23140402-1 Mp3 Player 100.00 0 Silver 1GB Size:1GB Colour:Silver
23140402-2 Mp3 Player 150.00 3 Silver 2GB Size:2GB Colour:Silver
23140402-3 Mp3 Player 100.00 6 Blue 1GB Size:1GB Colour:Blue
23140402-4 Mp3 Player 150.00 1 Blue 2GB Size:2GB Colour:Blue
23140402-5 Mp3 Player 300.00 9 Silver 8GB Size:8GB Colour:Silver
  • Get a generic order test working

  • Write modifier tests, and finish new modifiers code base

  • Turn modifiers system into a view-based system

  • Factor email, and status stuff out of Order class

  • Rewrite and improve pricing architecture

<?php
class WeightField extends TextField{
static $baseunit = 'kilogram';
//rounding?
//use gravity constant as basis?
static $unittable = array(
'kilogram' => array(
'convert' => 1,
'abbreviation' => 'kg'
),
'gram' => array(
'convert' => 0.001,
'abbreviation' => 'g'
),
'pound' => array(
'convert' => 0.45359237,
'abbreviation' => 'lb'
),
'ounce' => array(
'convert' => 0.0283495231,
'abbreviation' => 'oz'
)
);
function __construct($name, $title = null, $value = "", $form = null) {
// naming with underscores to prevent values from actually being saved somewhere
$this->fieldValue = parent::Field($name."[Amount]",$title,$value,$form);
$this->fieldUnit = $this->UnitField($name);
parent::__construct($name, $title, $value, $form);
}
function Field() {
return "<div class=\"fieldgroup\">" .
"<div class=\"fieldgroupField\">" . $this->fieldValue . "</div>" .
"<div class=\"fieldgroupField\">" . $this->fieldUnit . "</div>" .
"</div>";
}
function UnitField($name){
$unittypes = array(
'kilogram'
);
return new DropdownField("{$name}[Unit]",_t('WeightField.FIELDLABELUNIT', 'Unit'),$unittypes);
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment