VbzCart/code/files/shop.php/2011-12-18

About
This was an attempt to redo the API so sub-nodes would have better access to the whole script. It made things hopelessly tangled, so I had to abandon it and hope that I could work out another (better) way.

Code
<?php /* PURPOSE: vbz class 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 NAMES: define('KST_CART_DATA'		,'shop_cart_data');

// 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');

// FORM FIELD NAMES: // -- cart/shipping define('KSF_SHIP_ZONE'		,'ship-zone'); // -- shipping define('KSF_SHIP_TO_SELF'	,'ship-to-self');	// TRUE = use shipping info for recipient too define('KSF_SHIP_IS_CARD'	,'ship-is-billing');	// TRUE = shipping address same as billing/card define('KSF_SHIP_MESSAGE'	,'ship-message'); // address fields: prefix with "ship" or "card" as appropriate define('_KSF_ADDR_NAME'		,'addr-name'); define('_KSF_ADDR_STREET'	,'addr-street'); define('_KSF_ADDR_CITY'		,'addr-city'); define('_KSF_ADDR_STATE'	,'addr-state'); define('_KSF_ADDR_ZIP'		,'addr-zip'); define('_KSF_ADDR_COUNTRY'	,'addr-country'); define('_KSF_ADDR_EMAIL'	,'addr-email'); define('_KSF_ADDR_PHONE'	,'addr-phone');

// for retrieving field data: define('KSF_PFX_SHIP',		'ship-'); define('KSF_ADDR_SHIP_NAME'	,KSF_PFX_SHIP._KSF_ADDR_NAME); define('KSF_ADDR_SHIP_STREET'	,KSF_PFX_SHIP._KSF_ADDR_STREET); define('KSF_ADDR_SHIP_CITY'	,KSF_PFX_SHIP._KSF_ADDR_CITY); define('KSF_ADDR_SHIP_STATE'	,KSF_PFX_SHIP._KSF_ADDR_STATE); define('KSF_ADDR_SHIP_ZIP'	,KSF_PFX_SHIP._KSF_ADDR_ZIP); define('KSF_ADDR_SHIP_COUNTRY'	,KSF_PFX_SHIP._KSF_ADDR_COUNTRY); define('KSF_CUST_SHIP_EMAIL'	,KSF_PFX_SHIP._KSF_ADDR_EMAIL); define('KSF_CUST_SHIP_PHONE'	,KSF_PFX_SHIP._KSF_ADDR_PHONE);

// -- payment define('KSF_CUST_CARD_NUM'	,'cust-card-num'); define('KSF_CUST_CARD_EXP'	,'cust-card-exp'); define('KSF_CUST_CARD_NAME'	,'cust-card-name'); define('KSF_CUST_CARD_STREET'	,'cust-card-street'); define('KSF_CUST_CARD_CITY'	,'cust-card-city'); define('KSF_CUST_CARD_STATE'	,'cust-card-state'); define('KSF_CUST_CARD_ZIP'	,'cust-card-zip'); define('KSF_CUST_CARD_COUNTRY'	,'cust-card-country'); define('KSF_CUST_CHECK_NUM'	,'cust-check-num'); define('KSF_CUST_PAY_EMAIL'	,'cust-pay-email'); define('KSF_CUST_PAY_PHONE'	,'cust-pay-phone'); // DATA STORAGE KEYS: // -- shipping define('KSI_SHIP_ZONE'		,100);

define('KSI_ADDR_SHIP_NAME'	,101); define('KSI_ADDR_SHIP_STREET'	,102); define('KSI_ADDR_SHIP_CITY'	,103); define('KSI_ADDR_SHIP_STATE'	,104); define('KSI_ADDR_SHIP_ZIP'	,105); define('KSI_ADDR_SHIP_COUNTRY'	,106); define('KSI_SHIP_MESSAGE'	,107); define('KSI_SHIP_TO_SELF'	,110); define('KSI_SHIP_IS_CARD'	,113);

define('KSI_CUST_SHIP_EMAIL'	,111); define('KSI_CUST_SHIP_PHONE'	,112); //define('KSI_SHIP_MISSING'	,120); // -- payment define('KSI_CUST_CARD_NUM'	,202); define('KSI_CUST_CARD_EXP'	,203);

define('KSI_ADDR_CARD_NAME'	,204); define('KSI_CUST_CARD_NAME'	,KSI_ADDR_CARD_NAME);	// alias define('KSI_ADDR_CARD_STREET'	,205); define('KSI_CUST_CARD_STREET'	,KSI_ADDR_CARD_STREET);	// alias define('KSI_ADDR_CARD_CITY'	,206); define('KSI_CUST_CARD_CITY'	,KSI_ADDR_CARD_CITY); define('KSI_ADDR_CARD_STATE'	,207); define('KSI_CUST_CARD_STATE'	,KSI_ADDR_CARD_STATE); define('KSI_ADDR_CARD_ZIP'	,208); define('KSI_CUST_CARD_ZIP'	,KSI_ADDR_CARD_ZIP); define('KSI_ADDR_CARD_COUNTRY'	,209); define('KSI_CUST_CARD_COUNTRY'	,KSI_ADDR_CARD_COUNTRY);

define('KSI_CUST_PAY_EMAIL'	,211); define('KSI_CUST_PAY_PHONE'	,212); //define('KSI_CUST_MISSING'	,220); define('KSI_CUST_CHECK_NUM'	,230);

// calculated data define('KSI_ITEM_TOTAL'		,301); define('KSI_PER_ITEM_TOTAL'	,302); define('KSI_PER_PKG_TOTAL'	,303);

// ORDER MESSAGE TYPES // these reflect the values in the ord_msg_media table define('KSI_ORD_MSG_INSTRUC',	1);	// Instructions in submitted order define('KSI_ORD_MSG_PKSLIP',	2);	// Packing slip define('KSI_ORD_MSG_EMAIL',	3);	// Email define('KSI_ORD_MSG_PHONE',	4);	// Phone call define('KSI_ORD_MSG_MAIL',	5);	// Snail mail define('KSI_ORD_MSG_FAX',	6);	// Faxed message define('KSI_ORD_MSG_LABEL',	7);	// Shipping label (for delivery instructions) define('KSI_ORD_MSG_INT',	8);	// internal use - stored, not sent

global $vgaCartDataType;	// documentation says this shouldn't be necessary $vgaCartDataType = array (   KSI_SHIP_ZONE		=> 'ship zone',    KSI_ADDR_SHIP_NAME		=> 'ship-to name',    KSI_ADDR_SHIP_STREET	=> 'ship-to street',    KSI_ADDR_SHIP_CITY		=> 'ship-to city',    KSI_ADDR_SHIP_STATE		=> 'ship-to state',    KSI_ADDR_SHIP_ZIP		=> 'ship-to zipcode',    KSI_ADDR_SHIP_COUNTRY	=> 'ship-to country',    KSI_SHIP_MESSAGE		=> 'ship-to message',    KSI_SHIP_TO_SELF		=> 'ship to self?',    KSI_SHIP_IS_CARD		=> 'ship to = card?',    KSI_CUST_SHIP_EMAIL		=> 'ship-to email',    KSI_CUST_SHIP_PHONE		=> 'ship-to phone', //    KSI_SHIP_MISSING		=> 'ship-to missing info', // -- payment    KSI_CUST_CARD_NUM		=> 'card number',    KSI_CUST_CARD_EXP		=> 'card expiry',    KSI_ADDR_CARD_NAME		=> 'card owner',    KSI_ADDR_CARD_STREET	=> 'card street address',    KSI_ADDR_CARD_CITY		=> 'card address city',    KSI_ADDR_CARD_STATE		=> 'card address state',    KSI_ADDR_CARD_ZIP		=> 'card zipcode', KSI_ADDR_CARD_COUNTRY	=> 'card country', KSI_CUST_CHECK_NUM		=> 'check number', KSI_CUST_PAY_EMAIL		=> 'customer email', KSI_CUST_PAY_PHONE		=> 'customer phone',

KSI_ITEM_TOTAL		=> 'item total', KSI_PER_ITEM_TOTAL		=> 's/h per-item total', KSI_PER_PKG_TOTAL		=> 's/h package total', );

if (defined('LIBMGR')) { 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::Load('strings',__FILE__,__LINE__); clsLibMgr::Load('string.tplt',__FILE__,__LINE__); clsLibMgr::Load('tree',__FILE__,__LINE__); clsLibMgr::Load('vbz.store',__FILE__,__LINE__); } else { // assume all libraries are on path require_once(KFP_LIB.'/strings.php'); require_once(KFP_LIB.'/StringTemplate.php'); require_once('store.php'); require_once(KFP_LIB.'/tree.php'); }

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_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 abstract class clsPageShop extends clsPage { public function Sessions { return $this->Make('clsShopSessions'); }   public function Clients { return $this->Make('clsShopClients'); }   public function Carts { return $this->Make('clsShopCarts'); }   public function CartLines { return $this->Make('clsShopCartLines'); }   public function CartLog { return $this->Make('clsShopCartLog'); }   public function Orders { return $this->Make('clsOrders'); }   public function OrdLines { return $this->Make('clsOrderLines'); } /*   public function OrderLog { return $this->Make('clsOrderLog'); }   public function OrdMsgs { return $this->Make('clsOrderMsgs'); } /*   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 clsPageCart extends clsPageShop { protected $objSess; protected $objCart;

// process functions public function DoPreContent { $this->inCkout = FALSE;	// (2011-04-01) not sure why this is sometimes not getting set parent::DoPreContent; }   /*      INPUT: $iCaller is for debugging and is discarded; caller should pass __METHOD__ as the argument. */   public function GetObjects($iCaller) { $objSessions = $this->Sessions; $this->objSess = $objSessions->GetCurrent;	// get whatever session is currently applicable (existing or new) $this->objCart = $this->objSess->CartObj; $this->objCart->objSess = $this->objSess;	// used for logging }   public function Cart($iObj=NULL) { if (!is_null($iObj)) { $this->objCart = $iObj; }	return $this->objCart; }   protected function HandleInput { $this->GetObjects(__METHOD__); $this->objCart->CheckData;	// check for any form data (added items, recalculations, etc.)

$this->strSheet	= 'cart';	// cart stylesheet has a few different things in it

$this->strWikiPg	= ''; $this->strTitle	= 'Shopping Cart';	// Displayed title (page header) $this->strName	= 'shopping cart';	// HTML title $this->strTitleContext	= 'this is your'; // 'Tomb of the...'; $this->strHdrXtra	= ''; $this->strSideXtra	= ''; //'Cat #: '.$this->strReq; $this->strSheet	= KSQ_PAGE_CART;	// default }   protected function DoContent { echo $this->objCart->Render; } }

/* ==================== *\ \* ==================== */
 * -- HELPER CLASSES -- ||

/*==== PURPOSE: parent class for all cart contact nodes HISTORY: 2011-11-26 started experimentally abstract class clsContactNode extends clsTreeNode {

protected $actSave;	// script created to record this data; NULL = not created yet

public function __construct($iNodes=NULL) { parent::__construct($iNodes); $this->actSave = NULL; }   public function ScriptMe { return $this->actSave; } /*   protected function ScriptUp { if ($this->HasParent) { return $this->Parent->ScriptMe; } else { return NULL; }   }    public function ScriptRoot { if (is_null($this->Parent)) { throw new exception('Internal Error: Expecting parent, but it is NULL. CLASS: '.get_class($this).' CTRL: '.$this->CtrlName); }	return $this->Parent->ScriptRoot; }   /*      RETURNS: a "starter" script object to use for generating this object's scripts There's probably a better way to do this. */   public function ScriptStart { return new Script_Script; }   public function Engine { if (is_null($this->Parent)) { throw new exception('Object (class '.get_class($this).') has no parent.'); }	return $this->Parent->Engine; }   /*      INPUT: $iScript -- the script currently being built. Avoid adding directly to it; add a folder, then add to the folder. Pass the folder, not iScript, on to the next level. */   public function Save(Script_Script $iScript) { $acts = NULL; echo ' SAVING '.get_class($this); if ($this->AccessCount == 0) { $acts = $this->ScriptStart; $iScript->Add($acts,get_class($this).'.save');

$this->SaveThis($acts); $this->SaveSubs($acts); $this->SavePost($acts); } else { echo ' - SKIPPED'; }	$this->IncCount; $this->actSave = $acts; return $acts; }   abstract protected function SaveThis(Script_Script $iScript); protected function SavePost(Script_Script $iScript) {}

/*     ACTION: Save sub-nodes */   protected function SaveSubs(Script_Script $iScript) { if ($this->HasNodes) { $acts = $this->ScriptStart; $iScript->Add($acts,get_class($this).'.subs');

$ar = $this->Nodes; foreach ($ar as $name => $obj) { $obj->Save($acts); }	} else { $acts = NULL; }	return $acts; } } /*==== PURPOSE: a contact node that holds other nodes but has no data of its own HISTORY: 2011-11-29 created class clsContactFolder extends clsContactNode { protected function SaveThis(Script_Script $iScript) {} 	// nothing to save here; only saves sub-nodes } class clsContactFolder_reference extends clsContactFolder { protected function SaveSubs(Script_Script $iScript) {}	// do not save sub-nodes; they are for reference only

/*     OVERRIDE: does not set the nodes' parents, because they may be parented elsewhere Otherwise we might get recursion. */ /* not sure if this is actually necessary; the problem seems to be something else protected function NodeAdd($iName,clsTreeNode $iNode) { $this->arSubs[$iName] = $iNode; } } class clsContactRoot extends clsContactFolder { protected $objDB; protected $actSave;

public function __construct(clsPageShop $iDB) { $this->objDB = $iDB; $this->actSave = NULL; }   public function Engine { return $this->objDB; }   public function ScriptRoot { if (is_null($this->actSave)) { $this->actSave = $this->ScriptStart; }	return $this->actSave; }   public function Save(Script_Script $iScript) { global $dbgIdx; $dbgIdx = 0; $this->ResetCount; return parent::Save($iScript); } } /*==== PURPOSE: Generic cart data field, no special handling HISTORY: 2011-11-29 changed ancestry from clsTreeNode to clsContactNode class clsCartField extends clsContactNode { protected $objCart; protected $intIndex;	// index of field type protected $strCtrlName;	// name of control (on form)

public function __construct($iCart, $iIndex, $iCtrlName) { if (is_object($iIndex)) { echo 'Internal error: argument $iIndex is an object of class '.get_class($iIndex).', not a scalar. ';	   throw new exception('Unexpected argument type.'); }	$this->objCart = $iCart; $this->intIndex = $iIndex; $this->strCtrlName = $iCtrlName; }   protected function SaveThis(Script_Script $iScript) { } 	// define later public function DataType { return $this->intIndex; }   public function Value($iVal=NULL) { // overrides default action by loading data from database if needed if (!is_null($iVal)) { die('writing to read-only clsTreeNode object'); // this should never happen, because we're not using this class to write these values }	if (!$this->Loaded) { if (is_null($this->intIndex)) { $this->vVal = NULL; } else { $this->vVal = $this->objCart->DataItem($this->intIndex); }	} else { }	return $this->vVal; }   public function CtrlName { return $this->strCtrlName; } }

class clsCartAddr extends clsContactFolder { //class clsCartAddr extends clsTreeNode { public $doFixed; public $doFixedCountry;	// Country is sometimes determined by Zone public $strStateLabel;	// label for "State" field public $strStatePost;	// text for after "State" field public $lenStateField;	// approximate width of "State" field public $strZipLabel;	// label for "Zipcode"/"Postal code" field

//   protected function SaveThis { } 	// stubbed off for customer-side pages /*     OVERRIDE: Name is saved normally, but other data is saved in aggregate by SaveThis */   protected function SaveSubs(Script_Script $iScript) { return $this->Name->Save($iScript); }

public function Name { return $this->Node('name'); }   public function Street { return $this->Node('street'); }   public function City { return $this->Node('city'); }   public function State { return $this->Node('state'); }   public function Zip { return $this->Node('zip'); }   public function Country { return $this->Node('country'); }   public function HasCountry { return $this->Exists('country'); }   public function Instruc { return $this->Node('instruc'); }   public function Render($iCart) {

// copy calculated stuff over to variables to make it easier to insert in formatted output: $ksName		= $this->Name->CtrlName; $ksStreet	= $this->Street->CtrlName; $ksCity		= $this->City->CtrlName; $ksState	= $this->State->CtrlName; $ksZip		= $this->Zip->CtrlName; $ksCountry	= $this->Country->CtrlName;

$strName	= $this->Name->Value; $strStreet	= $this->Street->Value; $strCity	= $this->City->Value; $strState	= $this->State->Value; $strZip		= $this->Zip->Value; $strCountry	= $this->Country->Value;

$strStateLabel	= $this->strStateLabel; $strZipLabel	= $this->strZipLabel; $lenState	= $this->lenStateField; $strStateAfter	= $this->strStatePost;

if ($this->doFixedCountry) { $htCountry = .$strCountry.; $htZone = ''; } else { $htCountry = ''; $htShipCombo = $this->htShipCombo; $htBtnRefresh = ''; $htZone = " - change shippping zone: $htShipCombo $htBtnRefresh"; }

if ($this->doFixedName) { $out = <<<__END__ Name: $strName __END__; } else { $out = <<<__END__ Name:  __END__; }	$out .= $this->htmlBeforeAddress; if ($this->doFixed) { $out .= <<<__END__ Street Address or P.O. Box: $strStreet City: $strCity $strStateLabel: $strState $strZipLabel: $strZip Country: $htCountry$htZone __END__; } else { $out .= <<<__END__ Street Address or P.O. Box: $strStreet City: <input name="$ksCity" value="$strCity" size=20> <td align=right valign=middle>$strStateLabel: <input name="$ksState" value="$strState" size=$lenState>$strStateAfter <td align=right valign=middle>$strZipLabel: <input name="$ksZip" value="$strZip" size=11> <td align=right valign=middle>Country: $htCountry - change shippping zone: $htShipCombo $htBtnRefresh __END__; }	return $out; }   /*      RETURNS: Complete address as single string, in multiple lines HISTORY: 2010-09-13 Added a line for Instruc */   public function AsText($iLineSep="\n") { $xts = new xtString($this->Street->Value,TRUE); $xts-> ReplaceSequence(chr(8).' ',' ',0);	// replace any blank sequences with single space $xts->ReplaceSequence(chr(10).chr(13),$iLineSep,0);	// replace any sequences of newlines with line sep string

$xts->Value .= $iLineSep.$this->City->Value; if ($this->State->Filled) { $xts->Value .= ', '.$this->State->Value; }	if ($this->Zip->Filled) { $xts->Value .= ' '.$this->Zip->Value; }	if ($this->Country->Filled) { $xts->Value .= ' '.$this->Country->Value; }	if ($this->Instruc->Filled) { $xts->Value .= $iLineSep.$this->Instruc->Value; }	return $xts->Value; }   public function AsSingleLine { return $this->AsText(' / '); }   public function AsSearchable { $out = $this->Street->Value; $out .= $this->City->Value; $out .= $this->State->Value; $out .= $this->Zip->Value; /* including the country when creating the search string causes issues: if the country is domestic, then we don't want to include it (because many people won't)... so we need to have some way of determining whether this is so -- but that's currently locked inside the ShipZone data, which isn't always available. Need to have a mapping of countries to zones, and more rigorous handling of country inputs.

For now, just leave it out (how likely is it that an address in another country will match, including postal code?) //	$out .= $this->Country->Value; $out = clsCustAddrs::Searchable($out); return $out; } } // contact fields class clsCartContact extends clsContactFolder { //   public $Addr; public $doFixed;

/*     HISTORY: 2011-10-05 Apparently, when addr is the same for payment and shipping, the default is to put it under payment rather than with the other contact info (phone, email).

If I can't figure out why this is, I'll probably switch it around later.

In any case, this looks first locally, and then under (parent)->payment */   public function Addr { if ($this->Exists('addr')) { return $this->Node('addr'); } elseif ($this->Parent->Exists('payment')) { $objPay = $this->Parent->Node('payment'); if ($objPay->Exists('addr')) { return $objPay->Node('addr'); }	}	return NULL; }   public function HasEmail { return $this->Exists('email'); }   public function Email { return $this->Node('email'); }   public function HasPhone { return $this->Exists('phone'); }   public function Phone { return $this->Node('phone'); }   protected function Person { return $this->Parent; }   public function Render { $hrefForSpam = '<a href="'.KWP_WIKI.'Anti-Spam_Policy">';

// copy any needed constants over to variables for parsing: $ksEmail	= $this->Email->CtrlName; $ksPhone	= $this->Phone->CtrlName;

$strEmail	= $this->Email->Value; $strPhone	= $this->Phone->Value;

if ($this->doFixed) { $out = <<<__END__ <td align=right valign=middle>Email: $strEmail <td align=right valign=middle>Phone: $strPhone __END__; } else { $out = <<<__END__ <td align=right valign=middle>Email: <input name="$ksEmail" value="$strEmail" size=30> {$hrefForSpam}anti-spam policy</a> <td align=right valign=middle>Phone: <input name="$ksPhone" value="$strPhone" size=20> (optional) __END__; }	return $out; }

} // a Payment has an Address, a Number (/CVV/Exp) and a Name class clsPayment extends clsContactFolder { //public $Name; // is this needed? Can be found at Addr->Name //   public $Addr;

//   protected function SaveThis { }	// stubbed for customer-side

public function CustName { return $this->Addr->Node('name'); }   public function Addr { return $this->Node('addr'); }   /*-      USED BY: admin functions */   public function MakeAddr($iVal) { $objNode = new clsTreeNode; $objNode->Value($iVal); $this->Node('addr',$objNode); }   public function Num { return $this->Node('num'); }   /*-      USED BY: admin functions */   public function MakeNum($iVal) { $objNode = new clsTreeNode; $objNode->Value($iVal); $this->Node('num',$objNode); }   public function Exp { return $this->Node('exp'); }   /*-      USED BY: admin functions */   public function MakeExp($iVal) { $objNode = new clsTreeNode; $objNode->Value($iVal); $this->Node('exp',$objNode); }   /*-      INPUT: iMaxFuture: if year is given as 2 digits, then this is the furthest in the future the year is allowed to be (# of years from now). NOTE: Should be tested with current dates after 2050 (or between 1950 and 1999) to make sure it doesn't allow a year too far in the past. OUTPUT: EXP as a DateTime object */   public function ExpDate($iMaxFuture=50) { $strExp = $this->Exp->Value; return clsCustCards::ExpDate($strExp,$iMaxFuture); }   public function ExpDateSQL { return clsCustCards::ExpDateSQL($this->Exp->Value); }   public function CVV { if ($this->Exists('cvv')) { return $this->Node('cvv')->Value; } else { return NULL; }   }    /*      ACTION: Return a description of the payment in a safe format (incomplete credit card number) TO DO: Allow for payment types other than credit card */   public function SafeDisplay { $out = clsCustCards::SafeDescr_Long($this->Num->Value,$this->Exp->Value); $out .= ' '.$this->Addr->AsText("\n "); return $out; } } /* RULES: * a Person must have two mailing addresses: one for shipping, one for payment * these addresses may be the same * a Person must have an email address * a Person may have a phone number * email and phone are not tied to mailing address, just to Person * a Person may (for now, must) have a card * a card has one mailing address class clsPerson extends clsContactFolder { /*   public $Contact; public $Payment; protected $strName;	// for forms private $strDescr;	// for text messages

public function __construct($iName,$iDescr) { $this->strName = $iName; $this->strDescr = $iDescr; }   public function Descr($iDescr=NULL) { if (!is_null($iDescr)) { $this->strDescr = $iDescr; }	return $this->strDescr; }   public function CtrlName { return $this->strName; }   public function HasContact { return $this->Exists('contact'); }   public function Contact { return $this->Node('contact'); }   public function Payment { return $this->Node('payment'); }   //=== EMAIL functions /*     HISTORY: 2011-09-23 This totally wasn't working. Is now. clsTreeNode could probably use some tidying. */   public function HasEmail { $out = FALSE; if ($this->Exists('contact')) { $objCont = $this->Contact; if ($objCont->Exists('email')) { $objEmail = $objCont->Node('email'); $out = $objEmail->Filled; }	}	return $out; }   /*      HISTORY: 2011-09-23 written to simplify things */   public function GetEmail { if ($this->HasEmail) { $out = $this->Contact->Email->Value; } else { $out = NULL; }	return $out; }

//=== PHONE functions public function HasPhone { $out = FALSE; if ($this->Exists('contact')) { $objCont = $this->Contact; if ($objCont->Exists('phone')) { $objPhone = $objCont->Node('phone'); $out = $objPhone->Filled; }	}	return $out; }   /*      HISTORY: 2011-09-23 written to simplify things */   public function GetPhone { if ($this->HasPhone) { $out = $this->Contact->Phone->Value; } else { $out = NULL; }	return $out; }

//=== CCARD functions public function HasCCard { $out = $this->Exists('payment'); return $out; }   /*      HISTORY: 2011-09-23 written to simplify things NOTE: slightly different from email/phone; returns object, not string */   public function GetCCard { if ($this->HasCCard) { return $this->Payment; } else { return NULL; }   }

//===   /*      HISTORY: 2011-11-21 fixed a bug where $objAddr was being called as a function, so presumably this method was not working before now. */   public function Addrs { $arOut = NULL; if ($this->HasCCard) { $arOut['card'] = $this->Payment->Addr; }	$objAddr = $this->Contact->Addr; if (!is_null($objAddr)) { $arOut['ship'] = $objAddr; }	return $arOut; }   /*      PURPOSE: The scripted version of DoResolve. EXPERIMENTAL/DEBUGGING HISTORY: 2011-09-21 started 2011-10-08 mostly working 2011-11-21 Script_DataRow was being called as a function instead of new object; fixed. Completed other identifier name changes. INPUT: data: depends on which contact data is being resolved; there can be 1 or 2 different sets $iOrder - changes to be made to the order record This function does not actually execute this, but only modifies it. It is up to the caller to execute it *after* calling this. $iContact - ID of existing contact record to update, or NULL if a new one should be created */ /* 2011-11-29 currently trying to obsolete this public function DoResolve_Script(Script_SQL_DataRow_Command $iOrder, $iShipSelf=FALSE) { global $wgRequest;

$objPerson = $this; $strStat = NULL;

$strFormName = $objPerson->FormName; $strChoice = $wgRequest->GetText($strFormName);

$doNew = FALSE; $doUpd = FALSE;

if ($strChoice == 'new') { // creating completely new contact record $doNew = TRUE; $strStat = 'Creating new contact record(s)'; $idContact = NULL; } elseif (is_numeric($strChoice)) { // updating existing contact record (possibly creating new detail records) $doUpd = TRUE; $idContact = (int)$strChoice; $strStat = 'Using contact ID='.$idContact; } elseif (empty($strChoice)) { $strStat = 'Please choose which contact to use.'; } else { throw new exception('Unexpected value for contact ID: ['.$strChoice.']'); }

$acts = new Script_Script; $acts->Add(new Script_Status($strStat));	// display the status message

if ($doNew || $doUpd) { // get some data for later use $strEmail = $objPerson->GetEmail; $strPhone = $objPerson->GetPhone; $objCCard = $objPerson->GetCCard;

// get some objects for later use $objDB = $iOrder->Engine; $tblEmails = $objDB->CustEmails; $tblPhones = $objDB->CustPhones; $tblCCards = $objDB->CustCards;

if ($doNew) { $objContact = $objPerson->Contact; if (is_object($objContact)) { // data integrity check successful

$objAddr = $objContact->Addr; if (is_null($objAddr)) { $acts->Add(new Script_Status('No address info')); } else { // we have some address data to record, so record it

$actCust = $objDB->Custs->Make_fromCartAddr_SQL($objAddr); $acts->Add(new Script_Status('ADDING CONTACT ADDRESS (check this!)')); $acts->Add($actCust,'cust');

// there needs to be a less script-structure-dependent way to do this: //$actCustIns = $actCust->Trial;	// get the insert action $actCustIns = $actCust->Get_byName('cust.ins',TRUE); $actCustXfer = $actCust->Get_byName('cust.id.xfer',TRUE);

// make sure email information is saved if (!is_null($strEmail)) {

// CHANGE TO Make_Script

$actCustXfer->Add($tblEmails->Script_forAdd($strEmail, $actCustIns),'cust.email'); }			// make sure phone information is saved if (!is_null($strPhone)) {

// CHANGE TO Make_Script

$actCustXfer->Add($tblPhones->Script_forAdd($strPhone, $actCustIns),'cust.phone'); }			// make sure credit card information is saved if (!is_null($objCCard)) { $actAddr = $actCust->Get_byName('cust.addr',TRUE); $actCCard = $tblCCards->Make_Script($idCust, $objCCard); $actCustXfer->Add($actCCard,'cust.card'); //$actCustXfer->Add($tblCCards->Script_forAdd($objCCard, $actCustIns, $actAddr),'cust.card'); //$actFill_ID_toCard = new Script_SQL_Use_ID($actAddr,$arCardCreate,'ID_Cust'); }		   }

} else { throw new exception('Person object has no Contact object.'); }	   }

if ($doUpd) { $idCust = (int)$strChoice; $strStat .= 'resolved as contact ID='.$idCust;

$objAddrs = $objPerson->Addrs;	// list of addresses from cart data

echo $objPerson->DumpHTML; die;

// create records for all names (will be either 1 or 2) associated with the order $tblNames = $objDB->CustNames;	// customer names table $tblAddrs = $objDB->CustAddrs;	// customer addresses table $out = NULL; if (!is_null($objAddrs)) {

foreach ($objAddrs as $type => $obj) {

if (!is_object($obj)) { throw new exception('value for '.$type.' is not an object.'); }

$this->ImportContact($acts,$type,$obj); /*			$acts->Add(new Script_Status('CHECKING '.$type));

$strName = $obj->Name->Value; $strKey = strtolower($strName);

$acts->Add(new Script_Status('ADDING NAME: '.$strName));

$actOrdScratch = new Script_RowObj_scratch('scratch order'); // ^ this will be copied to order update action ($iOrder) after name & address are set

// only add names that are different //if (array_key_exists($strKey,$arNames)) { $objName = $tblNames->Find($strKey,$idCust); if ($objName->HasRows) { $objName->FirstRow; $id = $objName->KeyValue; $acts->Add(new Script_Status('SAME as existing name ID='.$id)); $actOrdScratch->Value('name.'.$strFormName,$id); } else { $arAct = $tblNames->Create_SQL($idCust,$strName); $act = new Script_Tbl_Insert($tblNames,$arAct); $acts->Add(new Script_SQL_Use_ID($act,$actOrdScratch,'name')); }

$strKey = $obj->AsSearchable; $acts->Add(new Script_Status('ADDING ADDRESS: '.$strKey));

$acts->Add($tblAddrs->Make_Script($obj,$idCust,$actOrdScratch));

switch ($type) { case 'card': $sqlOrdContField = 'ID_Buyer'; $sqlOrdNameField = 'ID_NameBuyer'; $sqlOrdAddrField = NULL; break; case 'ship': $sqlOrdContField = 'ID_Recip'; $sqlOrdNameField = 'ID_NameRecip'; $sqlOrdAddrField = 'ID_ContactAddrRecip'; break; }			$arXl = array(			 $sqlOrdAddrField	=> 'addr',			  $sqlOrdNameField	=> 'name'			  ); $iOrder->Value($sqlOrdContField,$idCust); echo 'xfer: '.print_r($arXl,TRUE).' '; echo 'ord scratch: '.print_r($actOrdScratch->Values,TRUE).' '; $acts->Add(new Script_Copy_Named($actOrdScratch,$iOrder,$arXl),'order.update.'.$strFormName); //echo '#1: '.print_r($iOrder->Values,TRUE).' '; // the name is just for readability/debugging; it is not used by code /*		   }		}

// handle email address if (is_null($strEmail)) { $acts->Add(new Script_Status('No email address given.')); } else { // make new email record, unless an identical one exists: $acts->Add(new Script_Status('Adding email '.$strEmail)); $act = $tblEmails->Make_Script($idCust, $strEmail); $acts->Add($act); }

// handle phone number if (is_null($strPhone)) { $acts->Add(new Script_Status('No phone number given.')); } else { // make new phone record, unless an identical one exists: $acts->Add(new Script_Status('Adding phone '.$strPhone)); // THIS FUNCTION HAS NOT BEEN WRITTEN YET -- reuse code from email class $act = $tblPhones->Make_Script($idCust, $strPhone); $acts->Add($act); }

// handle credit card information if (is_null($objCCard)) { $acts->Add(new Script_Status('No credit card information given.')); // currently, we're not expecting this, as ccard is required } else { $acts->Add(new Script_Status('Adding ccard '.$objCCard->SafeDisplay)); $act = $tblCCards->Make_Script($idCust, $objCCard); $acts->Add($act);

$actIns = $act->Get_byName('ccard.make',TRUE); $actCopy = new Script_SQL_Use_ID($actIns,$iOrder,'ID_ChargeCard'); $acts->Add($actCopy); }	   } /*  By this time, $iOrder should have data for updating these fields: * ID_Buyer * ID_Recip * ID_NameBuyer * ID_NameRecip * ID_ContactAddrRecip * ID_ChargeCard

Add one more thing: /*	   $iOrder->Value('WhenPrepped','NOW');	// mark the order as "prepped" //echo '#2: '.print_r($iOrder->Values,TRUE).' '; $acts->Add($iOrder,'ord.upd'); }

return $acts; } /* 2011-11-29 this can't possibly work -- $strFormName is used but never defined protected function ImportContact(Script_Script $acts, $type, $obj) { $acts->Add(new Script_Status('CHECKING '.$type));

$strName = $obj->Name->Value; $strKey = strtolower($strName);

$acts->Add(new Script_Status('ADDING NAME: '.$strName));

$actOrdScratch = new Script_RowObj_scratch('scratch order'); // ^ this will be copied to order update action ($iOrder) after name & address are set

// only add names that are different //if (array_key_exists($strKey,$arNames)) { $objName = $tblNames->Find($strKey,$idCust); if ($objName->HasRows) { $objName->FirstRow; $id = $objName->KeyValue; $acts->Add(new Script_Status('SAME as existing name ID='.$id)); $actOrdScratch->Value('name.'.$strFormName,$id); } else { $arAct = $tblNames->Create_SQL($idCust,$strName); $act = new Script_Tbl_Insert($tblNames,$arAct); $acts->Add(new Script_SQL_Use_ID($act,$actOrdScratch,'name')); }

$strKey = $obj->AsSearchable; $acts->Add(new Script_Status('ADDING ADDRESS: '.$strKey));

$acts->Add($tblAddrs->Make_Script($obj,$idCust,$actOrdScratch));

switch ($type) { case 'card': $sqlOrdContField = 'ID_Buyer'; $sqlOrdNameField = 'ID_NameBuyer'; $sqlOrdAddrField = NULL; break; case 'ship': $sqlOrdContField = 'ID_Recip'; $sqlOrdNameField = 'ID_NameRecip'; $sqlOrdAddrField = 'ID_ContactAddrRecip'; break; }	$arXl = array(	 $sqlOrdAddrField	=> 'addr',	  $sqlOrdNameField	=> 'name'	  ); $iOrder->Value($sqlOrdContField,$idCust); echo 'xfer: '.print_r($arXl,TRUE).' '; echo 'ord scratch: '.print_r($actOrdScratch->Values,TRUE).' '; $acts->Add(new Script_Copy_Named($actOrdScratch,$iOrder,$arXl),'order.update.'.$strFormName); //echo '#1: '.print_r($iOrder->Values,TRUE).' '; // the name is just for readability/debugging; it is not used by code } // this is the OLD, non-scripted version (it didn't work very well either) public function DoResolve($iDB,$iContactID) { $objDB = $iDB; $objPerson = $this;

$strChoice = $iContactID;

if (!is_null($strChoice)) { $strStat = $objPerson->Descr.': ';

if ($strChoice == 'new') { $doNew = TRUE; $doAdd = FALSE; } else { $doNew = FALSE; if (is_numeric($strChoice)) { $doAdd = TRUE; } else { $doAdd = FALSE; }	   }

if ($doNew || $doAdd) { if ($doNew) { $objContact = $objPerson->Contact; if (is_object($objContact)) { // make new customer record, (shipping) address record, name record: $objCust = $objDB->Custs->Make_fromCartAddr($objContact->Addr); $idCust = $objCust->ID; $strStat .= 'created new contact ID='.$idCust; } else { throw new exception('Person object has no Contact object.'); }		}		if ($doAdd) { $idCust = (int)$strChoice; $strStat .= 'resolved as contact ID='.$idCust;

$objAddrs = $objPerson->Addrs; foreach ($objAddrs as $type => $obj) { $strStat .= ' / '.$type.':'; $strName = $obj->Name->Value; $arAction[] = 'making name record'; $idName = $objDB->CustNames->Create($idCust,$strName); $strStat .= ' ID_Name='.$idName; $arAction[] = 'making address record'; $idAddr = $objDB->CustAddrs->Create($idCust,$obj); $strStat .= ' ID_Addr='.$idAddr; $arAction[] = 'type='.$type.' idName='.$idName.' idAddr='.$idAddr.''; switch ($type) { case 'card': $idNameBuyer = $idName; $arOrdUpd['ID_NameBuyer'] = $idName; break; case 'ship': $idNameRecip = $idName; $idAddrRecip = $idAddr; $arOrdUpd['ID_NameRecip'] = $idName; $arOrdUpd['ID_ContactAddrRecip'] = $idAddr; break; }		   }		    $strStat .= ' / '; }		$this->SubValue('id',$idCust);	// 2011-09-22 what does this do? Where is it defined? //$arOrdUpd['ID_Cust'] = $idCust;

$arAction = NULL; $arUpdEv = NULL;

// make new email record, unless an identical one exists: if ($objPerson->HasEmail) { $strEmail = $objPerson->Contact->Email->Value; $arAction[] = 'making email record for ['.$strEmail.']'; $idEmail = $objDB->CustEmails->Create($idCust, $strEmail); /*		   $objDB->LogEvent(		      __METHOD__,		      '|idCust='.$idCust.'|strEmail='.SQLValue($strEmail),		      'Made email record; SQL='.SQLValue($sql),		      '+EM',FALSE,FALSE);

$arEv = array(		     'where'	=> __METHOD__,		      'params'	=> '|idCust='.$idCust.'|strEmail='.SQLValue($strEmail),		      'descr'	=> 'Made email record; SQL='.SQLValue($sql),		      'code'	=> '+EM'		      ); $arUpdEv[] = $arEv;

/*		   // 2009-11-28 at present, we don't attach specific email or phone IDs to orders or contacts $arOrdUpd['ID_Email'] = $idEmail; $strStat .= ' ID_Email='.$idEmail; */		}		// make new phone record, unless an identical one exists: if ($objPerson->HasPhone) { $strPhone = $objPerson->Contact->Phone->Value; $arAction[] = 'making phone record for ['.$strPhone.']'; $idPhone = $objDB->CustPhones->Create($idCust, $strPhone); /*		   $objDB->LogEvent(		      __METHOD__,		      '|idCust='.$idCust.'|strPhone='.SQLValue($strPhone),		      'Made phone record; SQL='.SQLValue($sql),		      '+PH',FALSE,FALSE); // ($iWhere,$iParams,$iDescr,$iCode,$iIsError,$iIsSevere		   $arEv = array( 'where'	=> __METHOD__, 'params'	=> '|idCust='.$idCust.'|strPhone='.SQLValue($strPhone), 'descr'	=> 'Made phone record; SQL='.SQLValue($sql), 'code'	=> '+PH' );		   $arUpdEv[] = $arEv;		    /*		    // 2009-11-28 at present, we don't attach specific email or phone IDs to orders or contacts		    $arOrdUpd['ID_Phone'] = $idPhone;		    $strStat .= ' ID_Phone='.$idPhone;		    */		}		// make new ccard record, unless an identical one exists:		if ($objPerson->HasCCard) {		    $arAction[] = 'making ccard record';		    $idCard = $objDB->CustCards->Create($idCust, $objPerson->Payment);		    $objDB->LogEvent( __METHOD__, '|idCust='.$idCust, 'Made ccard record; SQL='.SQLValue($sql), '+CC',FALSE,FALSE);		   assert('!empty($idCard) /* type='.get_class($objDB->CustCards).' */');		    $arOrdUpd['ID_ChargeCard'] = $idCard;		    $strStat .= ' ID_ChargeCard='.$idCard;		} // if customer is shipper, write ID_Name and ID_Addr back to customer record:		if (isset($idNameRecip)) {		    $arAction[] = 'updating contact with name/addr';		    $arUpd['ID_Name'] = $idNameRecip;		    $arUpd['ID_Addr'] = $idAddrRecip;		    $objDB->Custs->Update($arUpd,'ID='.$idCust);		} else {		    $arAction[] = 'recipient details not in this object';		}	    } else {		// log invalid input		$strStat = 'Could not resolve - invalid choice "'.$strChoice.'".';	    }	} else {	    $strStat = 'No contact specified for resolution (how did we even get here?).';	}	$arUpdOrd['upd-ord'] = $arOrdUpd;	$arUpdOrd['stat'] = $strStat;

$arOut['status'] = $strStat; $arOut['action'] = $arAction; $arOut['upd.ord'] = $arUpdOrd;

return $arOut; } } /*================== CLASS: clsShipZone PURPOSE: shipping zone functions class clsShipZone { 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 Text { global $listShipListDesc;

return $listShipListDesc[$this->Abbr]; }   public function hasState { switch ($this->Abbr) { case 'US':	return TRUE;	break; case 'CA':	return TRUE;	break; default:	return FALSE;	break; }   }    public function StateLabel { switch ($this->Abbr) { case 'US':	return 'State';		break; case 'CA':	return 'Province';	break; default:	return 'County/Province'; break; }   }    public function PostalCodeName { switch ($this->Abbr) { case 'US':	return 'Zip Code&trade;';	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 { global $listShipListDesc;

$strZoneCode = $this->Abbr; $out = '<select name="ship-zone">'; foreach ($listShipListDesc as $key => $descr) { //$dest (keys(%listShipListDesc)) { $strZoneDesc = $descr; if ($key == $strZoneCode) { $htSelect = " selected"; } else { $strZoneDesc .= " - recalculate"; $htSelect = ""; }		$out .= '<option'.$htSelect.' value="'.$key.'">'.$strZoneDesc.' '; }	$out .= ' '; return $out; } }

// ShopCart class clsShopCarts extends clsTable { const TableName='shop_cart';

public function __construct($iDB) { parent::__construct($iDB); $this->Name(self::TableName); $this->KeyName('ID'); $this->ClassSng('clsShopCart'); $this->ActionKey('cart'); } } class clsShopCart extends clsDataSet { public $objShipZone; private $arDataItem; protected $objOrder;

protected $hasDetails;	// customer details have been loaded? protected $objDataTree; protected $objAddrShip; protected $objAddrCard; protected $objContDest; protected $objContCust; protected $objShip; protected $objCust;

public function __construct(clsDatabase $iDB=NULL, $iRes=NULL, array $iRow=NULL) { parent::__construct($iDB,$iRes,$iRow); $this->objShipZone = new clsShipZone; $this->hasDetails = FALSE; }   public function InitNew($iSess) { $this->ID = 0; $this->WhenCreated = NULL;	// not created until saved $this->WhenViewed = NULL; $this->WhenUpdated = NULL; $this->WhenOrdered = NULL; $this->ID_Sess = $iSess; $this->ID_Order = NULL; $this->ID_Cust = NULL; }   /*====      BLOCK: EVENT HANDLING HISTORY: 2011-03-27 copied from VbzAdminCustCard to VbzAdminCart Then moved from VbzAdminCart to clsShopCart */   protected function Log { if (!is_object($this->logger)) { $this->logger = new clsLogger_DataSet($this,$this->objDB->Events); }	return $this->logger; }   public function StartEvent(array $iArgs) { return $this->Log->StartEvent($iArgs); }   public function FinishEvent(array $iArgs=NULL) { return $this->Log->FinishEvent($iArgs); }   public function EventListing { return $this->Log->EventListing; }   // specialized event logging (deprecated) public function LogEvent($iCode,$iDescr,$iUser=NULL) { global $vgUserName;

$strUser = is_null($iUser)?$vgUserName:$iUser; $this->objDB->CartLog->Add($this,$iCode,$iDescr,$strUser); }   //====    /*      HISTORY: 2010-12-31 Created so placed orders do not get "stuck" in user's browser 2011-02-07 Doesn't work; same cart still comes up (though at least it generates a new order...	 but it pulls up all the same contact info) 2011-03-27 Changed flag from ID_Order to WhenOrdered OR WhenVoided, because we don't want to have to clear ID_Order anymore. Carts should retain their order ID. */   public function IsLocked { return $this->IsOrdered || $this->IsVoided; }   /*      RETURNS: TRUE if the cart has been converted to an order USED BY: $this->IsLocked and (something)->IsUsable HISTORY: 2011-03-27 written for improved handling of cart status at checkout */   public function IsOrdered { return !(is_null($this->WhenOrdered)); }   /*      RETURNS: TRUE if the cart has been discarded (voided) USED BY: $this->IsLocked and (something)->IsUsable HISTORY: 2011-03-27 written for improved handling of cart status at checkout */   public function IsVoided { return !(is_null($this->WhenVoided)); } // == GENERAL DATA UTILITY FUNCTIONS public function GetDataItem($iType) { global $sql;	// for debugging

if (isset($this->arDataItem[$iType])) { return $this->arDataItem[$iType]; } else { $sqlType = $this->objDB->SafeParam($iType); $sql = 'SELECT Val FROM '.KST_CART_DATA.' WHERE (ID_Cart='.$this->ID.') AND (Type="'.$sqlType.'");';

$objItem = $this->objDB->DataSet($sql); if ($objItem->HasRows) { $objItem->FirstRow; return $objItem->Val; } else { return NULL; }	}   }    public function PutDataItem($iType,$iVal,$iForce=FALSE) { global $sql;	// for debugging

$sqlType = $this->objDB->SafeParam($iType); $sqlFilt = '(ID_Cart='.$this->ID.') AND (Type="'.$iType.'")'; if ($iVal == '') { if ($iForce) { // delete the entry $sql = 'DELETE FROM '.KST_CART_DATA.' WHERE '.$sqlFilt; $ok = $this->objDB->Exec($sql); return $ok; } else { return FALSE; }	 } else { $sqlVal = $this->objDB->SafeParam($iVal); // check to see if item exists (UPDATE) or not (INSERT): $objRows = $this->objDB->DataSet('SELECT Val FROM '.KST_CART_DATA.' WHERE '.$sqlFilt); $qrows = $objRows->RowCount; if ($qrows == 0) { $sql = 'INSERT INTO '.KST_CART_DATA.' (ID_Cart,Type,Val) VALUES('.$this->ID.','.$iType.',"'.$sqlVal.'");'; $ok = $this->objDB->Exec($sql); } elseif ($qrows == 1) { $sql = 'UPDATE '.KST_CART_DATA.' SET Val="'.$sqlVal.'" WHERE '.$sqlFilt.';'; $ok = $this->objDB->Exec($sql); } else { // the db should never let this happen since ID_Cart+Type is the primary key, but leaving in as a sanity check for now. $this->objDB->LogEvent('cart.data.write','type='.$iType.' val='.$sqlVal,$qrows.' rows match: too many','DUP',TRUE,TRUE); $ok = FALSE; }	     return $ok; }   }    public function DataItem($iType,$iVal=NULL,$iForce=FALSE) { if ($this->HasCart) { if (is_null($iVal)) { return $this->GetDataItem($iType); } else { return $this->PutDataItem($iType,$iVal,$iForce); }	} else { return NULL; }   } // == STATUS public function HasCart { return $this->IsCreated;	// may use different criteria later on   } public function HasSession { $ok = FALSE; if ($this->HasField('ID_Sess')) { if ($this->ID_Sess>0) { $ok = TRUE; }	}	return $ok; }   public function Session { if ($this->HasSession) { $objSessions = $this->objDB->Sessions; $objSess = $objSessions->GetItem($this->ID_Sess); return $objSess; } else { return NULL; }   }    // DEPRECATED - use OrderObj public function Order { return $this->OrderObj; }   /*      RETURNS: Order object */   public function OrderObj { $doGet = TRUE; if (isset($this->objOrder)) { if ($this->objOrder->ID == $this->ID_Order) { $doGet = FALSE; }	}	if ($doGet) { $this->objOrder = $this->objDB->Orders->GetItem($this->ID_Order); }	return $this->objOrder; }   public function HasLines { $objLines = $this->GetLines; if (is_null($objLines)) { return FALSE; } else { return $objLines->hasRows; }   }    public function LineCount { if ($this->HasLines) { return $this->objLines->RowCount; } else { return 0; }   }    public function GetLines($iRefresh=TRUE) { if ($iRefresh || (!isset($this->objLines))) { if ($this->IsCreated) { //$this->objLines = $this->objDB->CartLines->GetData('(ID_Cart='.$this->ID.') AND (Qty>0)','clsShopCartLine'); $this->objLines = $this->objDB->CartLines->GetData('(ID_Cart='.$this->ID.') AND (Qty>0)'); } else { $this->objLines = NULL; }	}	return $this->objLines; }   public function IsCreated { return ($this->ID > 0); //return !is_null($this->ID); //return $this->hasField('ID') || isset($this->ID) }   /*      RETURNS: TRUE iff customer has any known email addresses public function HasEmail { $isShipCard = $this->DataItem(KSI_SHIP_IS_CARD); $isShipSelf = $this->DataItem(KSI_SHIP_TO_SELF); // $objCart->ContCustObj->Email->Value if ($isShipSelf) { return $this->ContCustObj->HasEmail; }   public function EmailObj { } // == FORM HANDLING STUFF public function CheckData { // check for buttons $doCheckout = isset($_POST['finish']); $isCart = (isset($_POST['recalc']) || $doCheckout); $isZoneSet = FALSE; // check for specific actions if (isset($_GET['action'])) { $strDo = $_GET['action']; switch ($strDo) { case 'del': $intItem = 0+$_GET['item']; $this->GetLines; $this->objLines->Update(array('Qty'=>0),'ID_Item='.$intItem); $this->LogEvent('del','deleting from cart: ID '.$intItem); break; case 'delcart'; $this->LogEvent('clr','voiding cart'); $this->ID = -1; $this->objSess->DropCart; break; }	} else { foreach ($_POST as $key => $val) { // check for added items: if (substr($key,0,4) == 'qty-') { if (($val != '') && ($val != 0)) { $sqlCatNum = $this->objDB->SafeParam(substr($key,4)); if ($isCart) { // zero out all items, so only items in visible cart will be retained: $this->ZeroAll; }			$this->AddItem($sqlCatNum,$val); }		} elseif ($key == KSF_SHIP_ZONE) { //		   $custShipZone	= $this->GetFormItem(KSF_SHIP_ZONE); $custShipZone	= $val; $this->DataItem(KSI_SHIP_ZONE,$custShipZone); $this->objShipZone->Abbr($custShipZone); $isZoneSet = TRUE; }	   }	}	if (!$isZoneSet) { $this->objShipZone->Abbr($this->DataItem(KSI_SHIP_ZONE)); }	if ($doCheckout) { $this->LogEvent('ck1','going to checkout'); $objSess = $this->Session; //http_redirect(KWP_CHKOUT,array(KS_VBZCART_SESSION_KEY => $objSess->SessKey)); //http_redirect(KWP_CHKOUT.'?'.KS_VBZCART_SESSION_KEY.'='.$objSess->SessKey); http_redirect(KWP_CHKOUT); //	   http_redirect('https://ssl.vbz.net/phpinfo.php'); $this->LogEvent('ck2','sent redirect to checkout'); }   }    public function ZeroAll { $this->Update(array('Qty'=>0),'ID_Cart='.$this->ID); }   public function AddItem($iCatNum,$iQty) { $this->Build;	// make sure there's a record for the cart, get ID	$objCartLines = $this->objDB->CartLines; $objCartLines->Add($this->ID,$iCatNum,$iQty); $this->LogEvent('add','adding to cart: cat# '.$iCatNum.' qty '.$iQty); }   /*-      ACTION: * make sure there is a cart record * update the quantity, if there is one */   public function Build { $id = $this->ID; if (empty($id)) { $this->Create; }   }    public function Create { $sql = 'INSERT INTO `'.clsShopCarts::TableName.'` (WhenCreated,ID_Sess)'. 'VALUES(NOW,'.$this->ID_Sess.');'; $this->objDB->Exec($sql); $this->ID = $this->objDB->NewID('carts.create'); $objSess = $this->objDB->Sessions->GetCurrent; if (!is_object($objSess->Table)) { throw new exception('Session object has no table for Cart ID='.$this->Value('ID')); }	$objSess->SetCart($this->ID); }   public function RenderHdr { $out = "\n".''; $out .= "\n ';	$out .= ' '.$htEmailBody.' ';	if (!$doSend) {	   $out .= ' ';	    $out .= '<input type=hidden name=email-body value="'.$htEmailBody.'">';	    $out .= '<input type=hidden name=send-to-cust value="'.$intCustCopy.'">';	    $out .= '<input type=hidden name=addr-self value="'.$htEmailAddr_Self.'">';	    $out .= '<input type=hidden name=addr-cust value="'.$htEmailAddr_Cust.'">';	    $out .= '<input type=hidden name=subject value="'.$htSubj.'">';	    $out .= '<input type=submit name=btnSend value="Send email">';	    $out .= ' ';	}

if ($iReally) { // if we're actually sending, then actually send the email and log it:

// log attempt to send email (EM: email/manual) $arEv = array(	     'descr'	=> 'manual order email (self:'.$txtSelfCopy.' cust:'.$txtCustCopy.') Subject: '.$txtSubj,	      'code'	=> 'OEM',	      'where'	=> __METHOD__	      ); $iLog->StartEvent($arEv);

if ($iSendToCust) { // if being sent to customer. record the email in the messages table }	   $okSelf = TRUE; if ($iSendToSelf) { // send our copy of the email $okSelf = mail($iAddrSelf,$txtSubj.' (store copy)',$iMessage,"From: $iAddrCust"); }	   $okCust = TRUE; if ($iSendToCust) { // log the message we're trying to send global $vgUserName; $this->LogEmail(NULL,$vgUserName,'customer',$txtSubj,$iMessage);

// send the message to the customer $okCust = mail($iAddrCust,$txtSubj,$iMessage,"From: $iAddrSelf"); }

// log event completion if ($okSelf && $okCust) { $arEv = NULL; } else { $arEv = array(		 'descrfin'	=> "self ok:$okSelf | cust ok:$okCust",		  'error'	=> TRUE,		  'severe'	=> TRUE); }	   $this->FinishEvent($arEv); }	return $out; }   public function LogEmail($iPackage,$iTxtFrom,$iTxtTo,$iSubject,$iMessage) { $this->LogMessage(	 $iPackage,	  KSI_ORD_MSG_EMAIL,	  $iTxtFrom,	  $iTxtTo,	  $iSubject,	  $iMessage); }   public function LogMessage($iPackage,$iMethod,$iTxtFrom,$iTxtTo,$iSubject,$iMessage) { $this->objDB->OrdMsgs->Add(	 $this->ID,	  $iPackage,	  $iMethod,	  $iTxtFrom,	  $iTxtTo,	  $iSubject,	  $iMessage); } } class clsOrderLines extends clsTable { const TableName='ord_lines';

public function __construct($iDB) { parent::__construct($iDB); $this->Name(self::TableName); $this->KeyName('ID'); $this->ClassSng('clsOrderLine'); } } class clsOrderLine extends clsDataSet { public function Init_fromCartLine(clsShopCartLine $iLine) { // some fields get copied over directly $arNames = array(	 'Seq'		=> 'Seq',	  'ID_Item'	=> 'ID_Item',	  'Qty'		=> 'QtyOrd',	  'CatNum'	=> 'CatNum',	  'PriceItem'	=> 'Price',	  'ShipPkgDest'	=> 'ShipPkg',	  'ShipItmDest'	=> 'ShipItm',	  'DescText'	=> 'Descr',	  //'DescHtml'	=> 'DescrHTML'	// we may eventually add this field	  ); foreach($arNames as $srce => $dest) { $val = $iLine->Value($srce); $this->Value($dest,$val); }   }    /*      HISTORY: 2011-03-23 created for AdminPage */   protected $objItem, $idItem; public function ItemObj { $doLoad = TRUE; $id = $this->Value('ID_Item'); if (isset($this->idItem)) { if ($this->idItem == $id) { $doLoad = FALSE; }	}	if ($doLoad) { $this->objItem = $this->Engine->Items($id); $this->idItem = $id; }	return $this->objItem; }   /*      RETURNS: selling price if order line has no price, falls back to catalog item HISTORY: 2011-03-23 created for "charge for package" process */   public function PriceSell { $prc = $this->Value('Price'); if (is_null($prc)) { $prc = $this->ItemObj->PriceSell; }	return $prc; }   /*      RETURNS: shipping per-package price if order line has no per-package price, falls back to catalog item HISTORY: 2011-03-23 created for "charge for package" process */   public function ShPerPkg { $prc = $this->Value('ShipPkg'); if (is_null($prc)) { $prc = $this->ItemObj->ShPerPkg; }	return $prc; }   /*      RETURNS: shipping per-item price -- defaults to catalog item's data unless specified in package line HISTORY: 2011-03-23 created for "charge for package" process */   public function ShPerItm { $prc = $this->Value('ShipItm'); if (is_null($prc)) { $prc = $this->ItemObj->ShPerItm; }	return $prc; }   /*      RETURNS: array of calculated values for this order line array[sh-pkg]: shipping charge per package array[sh-itm.qty]: shipping charge per item, adjusted for quantity ordered array[cost-sell.qty]: selling cost, adjusted for quantity ordered USED BY: so far, only admin functions (shopping functions use Cart objects, not Order) */   public function FigureStats { $qty = $this->Value('QtyOrd'); if ($qty != 0) { $prcShPkg = $this->ShPerPkg; } else { // none of this item in package, so don't require this minimum $prcShPkg = 0; }	$arOut['sh-pkg'] = $prcShPkg; $arOut['sh-itm.qty'] = $this->ShPerItm * $qty; $arOut['cost-sell.qty'] = $this->PriceSell * $qty; return $arOut; }   /*      ACTION: Figures totals for the current rowset USED BY: so far, only admin functions (shopping functions use Cart objects, not Order) RETURNS: array in same format as FigureStats, except with ".qty" removed from index names */   public function FigureTotals { $arSum = NULL; while ($this->NextRow) { $ar = $this->FigureStats;

$prcShItmSum = nzArray($arSum,'sh-itm',0); $prcShPkgMax = nzArray($arSum,'sh-pkg',0); $prcSaleSum = nzArray($arSum,'cost-sell',0);

$prcShItmThis = $ar['sh-itm.qty']; $prcShPkgThis = $ar['sh-pkg']; $prcSaleThis = $ar['cost-sell.qty'];

$arSum['sh-itm'] = $prcShItmSum + $prcShItmThis; $arSum['cost-sell'] = $prcSaleSum + $prcSaleThis; if ($prcShPkgMax < $prcShPkgThis) { $prcShPkgMax = $prcShPkgThis; }	   $arSum['sh-pkg'] = $prcShPkgMax; }	return $arSum; }   /*      ACTION: Render the current order line using static HTML (no form elements; read-only) HISTORY: 2011-04-01 adapted from clsShopCartLine::RenderHtml to clsOrderLine::RenderStatic */   public function RenderStatic(clsShipZone $iZone) { // calculate display fields: $qty = $this->Value('QtyOrd'); if ($qty) { //$this->RenderCalc($iZone);

$htLineCtrl = $qty;

$mnyPrice = $this->Value('Price');	// item price $mnyPerItm = $this->Value('ShipItm');	// per-item shipping $mnyPerPkg = $this->Value('ShipPkg');	// per-pkg minimum shipping $intQty = $this->Value('QtyOrd'); $mnyPriceQty = $mnyPrice * $intQty;		// line total sale $mnyPerItmQty = $mnyPerItm * $intQty;	// line total per-item shipping $mnyLineTotal = $mnyPriceQty + $mnyPerItmQty;	// line total overall (does not include per-pkg minimum)

$strCatNum = $this->Value('CatNum'); $strPrice = FormatMoney($mnyPrice); $strPerItm = FormatMoney($mnyPerItm); $strPriceQty = FormatMoney($mnyPriceQty); $strPerItmQty = FormatMoney($mnyPerItmQty); $strLineTotal = FormatMoney($mnyLineTotal);

$strShipPkg = FormatMoney($mnyPerPkg);

$htDesc = $this->Value('Descr');	// was 'DescHtml', but that field doesn't exist here

$htDelBtn = '';

$out = <<<__END__ $htDelBtn$strCatNum $htDesc <td class=cart-price align=right>$strPrice <td class=shipping align=right>$strPerItm <td class=qty align=right>$htLineCtrl <td class=cart-price align=right>$strPriceQty <td class=shipping align=right>$strPerItmQty <td class=total align=right>$strLineTotal <td class=shipping align=right>$strShipPkg __END__; return $out; }   } }

/*-- CLASS PAIR: order messages (table ord_msg) class clsOrderMsgs extends clsTable { public function __construct($iDB) { parent::__construct($iDB); $this->Name('ord_msg'); $this->KeyName('ID'); $this->ClassSng('clsOrderMsg'); }   /*      ACTION: Adds a message to the order */   public function Add($iOrder,$iPackage,$iMethod,$iTxtFrom,$iTxtTo,$iSubject,$iMessage) { global $vgUserName;

$arIns = array(	 'ID_Ord'	=> $iOrder,	  'ID_Pkg'	=> SQLValue($iPackage),	// might be NULL	  'ID_Media'	=> SQLValue($iMethod),	  'TxtFrom'	=> SQLValue($iTxtFrom),	  'TxtTo'	=> SQLValue($iTxtTo),	  'TxtRe'	=> SQLValue($iSubject),	  'doRelay'	=> 'FALSE',	// 2010-09-23 this field needs to be re-thought	  'WhenCreated'	=> 'NOW',	// later: add this as an optional argument, if needed	  'WhenEntered'	=> 'NOW',	  'WhenRelayed' => 'NULL',	  'Message'	=> SQLValue($iMessage)	  ); $this->Insert($arIns); } } class clsOrderMsg extends clsDataSet { }

/* ======= CREDIT CARD UTILITY CLASS class clsCustCards extends clsTable { public function __construct($iDB) { parent::__construct($iDB); $this->Name('cust_cards'); $this->KeyName('ID'); }

// STATIC section //

public static function CardTypeChar($iNum) { $chDigit = substr($iNum,0,1); $arDigits = array(	 '3' => 'A',	  '4' => 'V',	  '5' => 'M',	  '6' => 'D'	  ); if (isset($arDigits[$chDigit])) { $chOut = $arDigits[$chDigit]; } else { $chOut = '?'.$chDigit; }	return $chOut; }   public static function CardTypeName($iNum) { $chDigit = substr($iNum,0,1); $arDigits = array(	 '3' => 'Amex',	  '4' => 'Visa',	  '5' => 'MasterCard',	  '6' => 'Discover'	  ); if (isset($arDigits[$chDigit])) { $out = $arDigits[$chDigit]; } else { $out = '?'.$chDigit; }	return $out; }   public static function SafeDescr_Short($iNum,$iExp) { //$dtExp = strtotime($this->CardExp); $dtExp = self::ExpDate($iExp); if (is_null($dtExp)) { $strDate = '?/?'; } else { $strDate = $dtExp->format('n/y'); }	$out = self::CardTypeChar($iNum).'-'.substr($iNum,-4).'x'.$strDate; return $out; }   public static function SafeDescr_Long($iNum,$iExp) { $dtExp = self::ExpDate($iExp); if (is_null($dtExp)) { $strDate = '?/?'; } else { $strDate = $dtExp->format('F').' of '.$dtExp->format('Y').' ('.$dtExp->format('n/y').')'; }	$out = self::CardTypeName($iNum).' # ends with -'.substr($iNum,-4).' expires in '.$strDate; return $out; }   public static function Searchable($iRaw) { $xts = new xtString(strtolower($iRaw),TRUE); $xts->KeepOnly('0-9');	// keep only numerics return $xts->Value; }   /*-      INPUT: iMaxFuture: if year is given as 2 digits, then this is the furthest in the future the year is allowed to be (# of years from now). NOTE: Should be tested with current dates after 2050 (or between 1950 and 1999) to make sure it doesn't allow a year too far in the past. OUTPUT: EXP as a DateTime object */   public static function ExpDate($iRaw,$iMaxFuture=50) { $strExp = $iRaw; // -- split into month/year or month/day/year $arExp = preg_split('/[\/.\- ]/',$strExp); $intParts = count($arExp); switch ($intParts) { case 1:	// for now, we're going to assume MMYY[YY] // TO DO: if people start typing in M with no leading zero, will have to check for even/odd # of chars $intMo = substr($strExp,0,2); $intYr = substr($strExp,2); break; case 2:	// month/year $intMo = $arExp[0]; $intYr = $arExp[1]; break; case 3:	// month/day/year or year/month/day if (strlen($arExp[0]) > 3) { $intYr = $arExp[0]; $intMo = $arExp[1]; $intDy = $arExp[2]; } else { $intMo = $arExp[0]; $intDy = $arExp[1]; $intYr = $arExp[2]; }	   break; default: // unknown format, can't do anything }	// check for validity: $ok = FALSE; if (isset($intYr)) { if ($intYr > 0) { if (($intMo > 0) && ($intMo < 13)) { $ok = TRUE; }	   }	}	if ($ok) { if ($intYr < 100) {	// if year has no century, give it one $intYrNowPart = strftime('%y'); $intCent = (int)substr(strftime('%Y'),0,2); if ($intYr < $intYrNowPart) { $intCent++; }		$intYr += ($intCent*100); $intYrNowFull = (int)strftime('%Y'); if ($intYr - $intYrNowFull > $iMaxFuture) { $intYr -= 100; }	   }	    if (!isset($intDy)) { $intDy = cal_days_in_month(CAL_GREGORIAN, $intMo, $intYr);	// set to last day of month }	   $dtOut = $datetime = new DateTime; $dtOut->setDate($intYr, $intMo, $intDy); return $dtOut; } else { return NULL;	// if no year, then could not parse format }   }    public static function ExpDateSQL($iRaw) { $dt = self::ExpDate($iRaw); if (is_object($dt)) { return '"'.$dt->format('Y-m-d').'"'; } else { return 'NULL'; }   } }

/* =============== 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; }