VbzCart/docs/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>