VbzCart/archive/code/files/shop.php
About
- Purpose: classes needed when dealing with customer data (shopping cart etc.)
 - History:
- 2011-12-18 Saving current code just before making some API changes
 - 2013-02-20 About to rework part of the checkout -- combining shipping and payment pages; mostly works as-is, but iffy.
 
 - Alternatives:
- /2011-12-18 - got too ugly and complicated and unreliable
 
 
Code
<php> <?php /*
PURPOSE: vbz library for handling dynamic data related to shopping (cart, mainly) HISTORY: 2010-10-28 kluged the blank-order-email problem 2010-12-24 Fixed calls to Update() so they always pass arrays 2011-03-31 created AddMoney() and IncMoney() KLUGES: RenderReceipt() and TemplateVars() both have to reload the current record, which shouldn't be necessary.
- /
 
// FILE NAMES: define('KWP_ICON_ALERT' ,'/tools/img/icons/button-red-X.20px.png');
// TABLE ACTION KEYS
define('KS_URL_PAGE_SESSION',	'sess');
define('KS_URL_PAGE_ORDER',	'ord');	// must be consistent with events already logged
define('KS_URL_PAGE_ORDERS',	'orders');
if (!defined('LIBMGR')) {
require(KFP_LIB.'/libmgr.php');
}
clsLibMgr::Add('strings', KFP_LIB.'/strings.php',__FILE__,__LINE__); clsLibMgr::Add('string.tplt', KFP_LIB.'/StringTemplate.php',__FILE__,__LINE__); clsLibMgr::Add('tree', KFP_LIB.'/tree.php',__FILE__,__LINE__); clsLibMgr::Add('vbz.store', KFP_LIB_VBZ.'/store.php',__FILE__,__LINE__);
 clsLibMgr::AddClass('clsVbzPage', 'vbz.store');
clsLibMgr::Add('vbz.cart', KFP_LIB_VBZ.'/cart.php',__FILE__,__LINE__);
 clsLibMgr::AddClass('clsPageCart', 'vbz.cart');
 clsLibMgr::AddClass('clsShopCarts','vbz.cart');
clsLibMgr::Add('vbz.order', KFP_LIB_VBZ.'/orders.php',__FILE__,__LINE__);
 clsLibMgr::AddClass('clsOrders', 'vbz.order');
clsLibMgr::Load('strings',__FILE__,__LINE__); clsLibMgr::Load('string.tplt',__FILE__,__LINE__); clsLibMgr::Load('tree',__FILE__,__LINE__); clsLibMgr::Load('vbz.store',__FILE__,__LINE__); // clsLibMgr::Load('vbz.cart',__FILE__,__LINE__);
define('KS_VBZCART_SESSION_KEY','vbzcart_key');
// http query argument names define('KSQ_ARG_PAGE_DATA','page'); define('KSQ_ARG_PAGE_DEST','goto');
// http query values define('KSQ_PAGE_CART','cart'); // shopping cart define('KSQ_PAGE_SHIP','ship'); // shipping page define('KSQ_PAGE_PAY','pay'); // payment page define('KSQ_PAGE_CONT','cont'); // contact page -- shipping and payment define('KSQ_PAGE_CONF','conf'); // customer confirmation of order define('KSQ_PAGE_RCPT','rcpt'); // order receipt // if no page specified, go to the shipping info page (first page after cart): define('KSQ_PAGE_DEFAULT',KSQ_PAGE_SHIP);
/*
database class with creators for shop classes
- /
 
class clsVbzData_Shop extends clsVbzData {
   public function Sessions($id=NULL) {
return $this->Make('clsSessions_StoreUI',$id);
   }
   public function Clients($id=NULL) {
return $this->Make('clsShopClients',$id);
   }
   public function Carts($id=NULL) {
return $this->Make('clsShopCarts',$id);
   }
   public function CartLines($id=NULL) {
return $this->Make('clsShopCartLines',$id);
   }
   public function CartLog() {
return $this->Make('clsShopCartLog');
   }
   public function Orders($id=NULL) {
return $this->Make('clsOrders',$id);
   }
   public function OrdLines($id=NULL) {
return $this->Make('clsOrderLines',$id);
}
/*
   public function OrderLog() {
return $this->Make('clsOrderLog');
}
- /
 
   public function OrdMsgs($id=NULL) {
return $this->Make('clsOrderMsgs',$id);
}
/*
   public function Custs() {
return $this->Make('clsCusts');
   }
   public function CustNames() {
return $this->Make('clsCustNames');
   }
   public function CustAddrs() {
return $this->Make('clsCustAddrs');
   }
   public function CustEmails() {
return $this->Make('clsCustEmails');
   }
   public function CustPhones() {
return $this->Make('clsCustPhones');
   }
   public function CustCCards() {
return $this->Make('clsCustCards');
}
- /
 
}
/*==================
 CLASS: clsShipZone
 PURPOSE: shipping zone functions
 USAGE: Customize the isDomestic() function if you're shipping from somewhere other than the US
 RULES:
   * If a country's code isn't found in arDesc, it defaults to International
     ...there's got to be a better way to do this...
- /
 
class clsShipZone {
   static private $arDesc = array(
     'CA' => 'Canada',
     'US' => 'United States',
     'INT' => 'International',
     );
   // per-item adjustment factors
   static private $arItmFactors = array(
'US' => 1.0, 'CA' => 2.0, 'INT' => 4.0,
); // per-package adjustment factors static private $arPkgFactors = array( // there's got to be a better way to do this...
'US' => 1.0, 'CA' => 2.0, 'INT' => 4.0,
); static private $arCountryCodes = array(
'united states' => 'US', 'canada' => 'CA', 'australia' => 'AU',
);
private $strAbbr;
   public function Abbr($iAbbr=NULL) {
	if (!is_null($iAbbr)) {
	    $this->strAbbr = $iAbbr;
	}
	if (empty($this->strAbbr)) {
//echo '
RESETTING SHIPZONE; WAS '.$this->ShipZone;
	    $this->strAbbr = 'US';	// TO DO: set from configurable parameter
	}
	return $this->strAbbr;
   }
   public function Set_fromName($iName) {
$strLC = strtolower($iName); if (array_key_exists($strLC,self::$arCountryCodes)) { $this->strAbbr = self::$arCountryCodes[$strLC]; } else { echo 'Country ['.$iName.'] not found in list.'; throw new exception('Internal error: unknown country requested.'); }
   }
   public function Text() {	// should be Name()
return self::$arDesc[$this->Abbr()];
   }
   
   public function hasState() {
switch ($this->Abbr()) { case 'AU': return TRUE; break; case 'CA': return TRUE; break; case 'US': return TRUE; break; default: return FALSE; break; }
   }
   public function StateLabel() {
switch ($this->Abbr()) { case 'AU': return 'State/Territory'; break; case 'CA': return 'Province'; break; case 'US': return 'State'; break; default: return 'County/Province'; break; }
   }
   public function PostalCodeName() {
switch ($this->Abbr()) { case 'US': return 'Zip Code™'; break; default: return 'Postal Code'; break; }
   }
   public function Country() {
switch ($this->strAbbr) { case 'US': return 'United States'; break; case 'CA': return 'Canada'; break; default: return NULL; break; }
   }
   public function isDomestic() {
return ($this->Abbr() == 'US');
   }
   public function ComboBox() {
$strZoneCode = $this->Abbr(); $out = '<select name="ship-zone">'; foreach (self::$arDesc as $key => $descr) { //$dest (keys(%listShipListDesc)) { $strZoneDesc = $descr; if ($key == $strZoneCode) { $htSelect = " selected"; } else { $strZoneDesc .= " - recalculate"; $htSelect = ""; } $out .= '<option'.$htSelect.' value="'.$key.'">'.$strZoneDesc.'</option>'; } $out .= '</select>'; return $out;
   }
   /*----
     RETURNS: per-item price factor for the current shipping zone
   */
   protected function PerItemFactor() {
echo 'CODE=['.$this->Abbr().'] ITEM FACTOR=['.self::$arItmFactors[$this->Abbr()].']
';
	return self::$arItmFactors[$this->Abbr()];
   }
   /*----
     RETURNS: per-package price factor for the current shipping zone
   */
   protected function PerPkgFactor() {
return self::$arPkgFactors[$this->Abbr()];
   }
   /*----
     INPUT: base per-item shipping price
     RETURNS: calculated price for the current shipping zone
   */
   public function CalcPerItem($iBase) {
return $iBase * $this->PerItemFactor();
   }
   /*----
     INPUT: base per-package shipping price
     RETURNS: calculated price for the current shipping zone
   */
   public function CalcPerPkg($iBase) {
return $iBase * $this->PerPkgFactor();
}
}
// ShopCart Log class clsShopCartLog extends clsTable {
const TableName='shop_cart_event';
   public function __construct($iDB) {
parent::__construct($iDB); $this->Name(self::TableName); $this->KeyName('ID');
   }
   public function Add($iCart,$iCode,$iDescr,$iUser=NULL) {
global $vgUserName;
$strUser = is_null($iUser)?$vgUserName:$iUser; if ($iCart->hasField('ID_Sess')) { $idSess = $iCart->ID_Sess; } else { // this shouldn't happen, but we still need to log the event, and ID_Sess is NOT NULL: $idSess = 0; }
$edit['ID_Cart'] = $iCart->ID; $edit['WhenDone'] = 'NOW()'; $edit['WhatCode'] = SQLValue($iCode); $edit['WhatDescr'] = SQLValue($iDescr); $edit['ID_Sess'] = $idSess; $edit['VbzUser'] = SQLValue($strUser); $edit['Machine'] = SQLValue($_SERVER["REMOTE_ADDR"]); $this->Insert($edit);
}
}
/* ===================
CLASS: clsShopSessions PURPOSE: Handles shopping sessions
- /
 
class clsShopSessions extends clsTable {
protected $SessKey;
const TableName='shop_session';
   public function __construct($iDB) {
parent::__construct($iDB); $this->Name(self::TableName); $this->KeyName('ID'); $this->ClassSng('clsShopSession');
   }
   private function Create() {
//$objSess = new clsShopSession($this->objDB); $objSess = $this->SpawnItem(); $objSess->InitNew(); $objSess->Create(); return $objSess;
   }
   public function SetCookie($iSessKey=NULL) {
if (!is_null($iSessKey)) { $this->SessKey = $iSessKey; } setcookie(KS_VBZCART_SESSION_KEY,$this->SessKey,0,'/','.'.KS_STORE_DOMAIN);
   }
   public function GetCurrent() {
$okSession = FALSE; $objClient = NULL; $strSessKey = NULL; if (isset($_COOKIE[KS_VBZCART_SESSION_KEY])) { $strSessKey = $_COOKIE[KS_VBZCART_SESSION_KEY]; } if (!is_null($strSessKey)) { list($ID,$strSessRand) = explode('-',$strSessKey); $objSess = $this->GetItem($ID); $okSession = $objSess->IsValidNow($strSessRand); // do session's creds match browser's creds? } if (!$okSession) { // no current/valid session, so make a new one: // add new record... $objSess = $this->Create(); // generate new session key $strSessKey = $objSess->SessKey(); //setcookie(KS_VBZCART_SESSION_KEY,$strSessKey); $this->SetCookie($strSessKey); } return $objSess;
}
} /* ===================
CLASS: clsShopSession PURPOSE: Represents a single shopping session
- /
 
class clsShopSession extends clsDataSet {
private $objCart; private $objClient;
   public function __construct(clsDatabase $iDB=NULL, $iRes=NULL, array $iRow=NULL) {
parent::__construct($iDB,$iRes,$iRow); /* if (is_null($this->objDB)) {
echo '
';
	    throw new exception('Database not set in clsShopSession.');
	}
*/
	//$this->Table = $this->Engine()->Sessions();
    }
    public function InitNew() {
	$this->Token = RandomString(31);
	$this->ID_Client = NULL;
	$this->ID_Cart = NULL;
	$this->WhenCreated = NULL;	// hasn't been created until written to db
	$this->Client();
    }
    public function Create() {
	$sql =
	  'INSERT INTO `'.clsShopSessions::TableName.'` (ID_Client,ID_Cart,Token,WhenCreated)'.
	  'VALUES('.SQLValue($this->ID_Client).', '.SQLValue($this->ID_Cart).', "'.$this->Token.'", NOW());';
	$this->objDB->Exec($sql);
	$this->ID = $this->objDB->NewID('session.create');
	if (!$this->Client()->isNew) {
	    $this->Client()->Stamp();
	}
    }
    /*-----
      RETURNS: TRUE if the stored session credentials match current reality (browser's credentials)
    */
    public function IsValidNow($iKey) {
	$ok = ($this->Token == $iKey);
	if ($ok) {
	    $idClientWas = $this->ID_Client;
	    $objClient = $this->Client();
	    if ($idClientWas != $this->ID_Client) {
		// not an error, but could indicate a hacking attempt -- so log it, flagged as severe:
		$this->objDB->LogEvent(
		  'session.valid',
		  'KEY='.$iKey,' OLD-CLIENT='.$idClientWas.' NEW-CLIENT='.$this->ID_Client,
		  'stored session client mismatch','XCRED',FALSE,TRUE);
		$ok = FALSE;
	    }
	}
	return $ok;
    }
    public function SetCart($iID) {
	$this->ID_Cart = $iID;
	$this->Update(array('ID_Cart'=>$iID));
    }
    /*----
      ACTION: Drop the current cart, so that added items will create a new one
      HOW: Tell the cart to lock itself, but don't forget it.
	CartObj() checks the cart to see if it is locked, and gets a new one if so.
      USED BY: "delete cart" user button
    */
    public function DropCart() {
	//$this->ID_Cart = NULL;
	$this->CartObj()->Update(array('WhenVoided'=>'NOW()'));
    }
    public function SessKey() {
	return $this->ID.'-'.$this->Token;
    }
    /*----
      ACTION: Loads the cart object.
	* If ID_Cart is set, looks up that cart.
	* If that cart has been locked (which currently happens when the cart
	  is converted to an order but might mean other things in the future),
	  discard it and get a new one.
      INPUT: $iCaller is for debugging and is discarded; caller should pass __METHOD__ as the argument.
    */
    public function Cart() {	// DEPRECATED FORM
	return $this->CartObj();
    }
    public function Client() {
// if the session's client record matches, then load the client record; otherwise create a new one:
	if (!isset($this->objClient)) {
	    $this->objClient = NULL;
	    $objClients = $this->objDB->Clients();
	    if (!is_null($this->ID_Client)) {
		$this->objClient = $objClients->GetItem($this->ID_Client);
		if (!$this->objClient->IsValidNow()) {
		    $this->objClient = NULL;	// doesn't match current client; need a new one
// TO DO: this should invalidate the session and be logged somewhere.
// It means that a session has jumped to a new browser, which shouldn't happen and might indicate a hacking attempt.
		}
	    }
	    if (is_null($this->objClient)) {
		$this->objClient = $objClients->SpawnItem();
		$this->objClient->InitNew();
		$this->objClient->Build();
		$this->ID_Client = $this->objClient->ID;
	    }
	}
	return $this->objClient;
    }
}
class clsShopClients extends clsTable {
    const TableName='shop_client';
    public function __construct($iDB) {
	parent::__construct($iDB);
	  $this->Name(self::TableName);
	  $this->KeyName('ID');
	  $this->ClassSng('clsShopClient');
    }
}
class clsShopClient extends clsDataSet {
    public function __construct(clsDatabase $iDB=NULL, $iRes=NULL, array $iRow=NULL) {
	parent::__construct($iDB,$iRes,$iRow);
	//$this->Table = $this->objDB->Clients();
    }
    public function InitNew() {
	$this->ID = NULL;
	$this->Address = $_SERVER["REMOTE_ADDR"];
	$this->Browser = $_SERVER["HTTP_USER_AGENT"];
	$this->Domain = gethostbyaddr($this->Address);
	$this->CRC = crc32($this->Address.' '.$this->Browser);
	$this->isNew = TRUE;
    }
    public function IsValidNow() {
	return (($this->Address == $_SERVER["REMOTE_ADDR"]) && ($this->Browser == $_SERVER["HTTP_USER_AGENT"]));
    }
    public function Stamp() {
	$this->Update(array('WhenFinal'=>'NOW()'));
    }
    public function Build() {
    // update existing record, if any, or create new one
	$sql = 'SELECT * FROM '.clsShopClients::TableName.' WHERE CRC="'.$this->CRC.'";';
	$this->Query($sql);
	if ($this->hasRows()) {
	    $this->NextRow();	// get data
	    $this->isNew = FALSE;
	} else {
	    $strDomain = $this->objDB->SafeParam($this->Domain);
	    $strBrowser = $this->objDB->SafeParam($this->Browser);
	    $sql = 'INSERT INTO `'.clsShopClients::TableName.'` (CRC, Address, Domain, Browser, WhenFirst)'
	    .' VALUES("'.$this->CRC.'", "'.$this->Address.'", "'.$strDomain.'", "'.$strBrowser.'", NOW());';
	    $this->objDB->Exec($sql);
	    $this->ID = $this->objDB->NewID('client.make');
	}
    }
}
/* ===============
 UTILITY FUNCTIONS
*/
function RandomString($iLen) {
    $out = ;
    for ($i = 0; $i<$iLen; $i++) {
	$n = mt_rand(0,61);
	$out .= CharHash($n);
    }
    return $out;
}
function CharHash($iIndex) {
    if ($iIndex<10) {
	return $iIndex;
    } elseif ($iIndex<36) {
	return chr($iIndex-10+ord('A'));
    } else {
	return chr($iIndex-36+ord('a'));
    }
}
// this can later be adapted to be currency-neutral
// for now, it just does dollars
function FormatMoney($iAmount,$iPrefix=,$iPlus=) {
    if ($iAmount < 0) {
	$str = '-'.$iPrefix.sprintf( '%0.2f',-$iAmount);
    } else {
	$str = $iPlus.$iPrefix.sprintf( '%0.2f',$iAmount);
    }
    return $str;
}
/*
  HISTORY:
    2011-08-03 added round() function to prevent round-down error
*/
function AddMoney($iMoney1,$iMoney2) {
    $intMoney1 = (int)round($iMoney1 * 100);
    $intMoney2 = (int)round($iMoney2 * 100);
    $intSum = $intMoney1 + $intMoney2;
    return $intSum/100;
}
/*
  HISTORY:
    2011-08-03 added round() function to prevent round-down error
*/
function IncMoney(&$iMoney,$iMoneyAdd) {
    $intBase = (int)round(($iMoney * 100));
    $intAdd = (int)round(($iMoneyAdd * 100));
    $intSum = $intBase + $intAdd;
    $iMoney = $intSum/100;
}
</php>