Difference between revisions of "VbzCart/archive/code/files/shop.php"

from HTYP, the free directory anyone can edit if they can prove to me that they're not a spambot
< VbzCart‎ | archive‎ | code‎ | files
Jump to navigation Jump to search
(mostly working; about to significantly change part of checkout)
Line 3: Line 3:
 
* '''History''':
 
* '''History''':
 
** '''2011-12-18''' Saving current code just before making some API changes
 
** '''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''':
 
* '''Alternatives''':
 
** [[/2011-12-18]] - got too ugly and complicated and unreliable
 
** [[/2011-12-18]] - got too ugly and complicated and unreliable
Line 9: Line 10:
 
<?php
 
<?php
 
/*
 
/*
   PURPOSE: vbz class library for handling dynamic data related to shopping (cart, mainly)
+
   PURPOSE: vbz library for handling dynamic data related to shopping (cart, mainly)
 
   HISTORY:
 
   HISTORY:
 
     2010-10-28 kluged the blank-order-email problem
 
     2010-10-28 kluged the blank-order-email problem
Line 21: Line 22:
 
define('KWP_ICON_ALERT' ,'/tools/img/icons/button-red-X.20px.png');
 
define('KWP_ICON_ALERT' ,'/tools/img/icons/button-red-X.20px.png');
  
// TABLE NAMES:
 
define('KST_CART_DATA' ,'shop_cart_data');
 
  
 
// TABLE ACTION KEYS
 
// TABLE ACTION KEYS
Line 29: Line 28:
 
define('KS_URL_PAGE_ORDERS', 'orders');
 
define('KS_URL_PAGE_ORDERS', 'orders');
  
// FORM FIELD NAMES:
+
if (!defined('LIBMGR')) {
// -- cart/shipping
+
    require(KFP_LIB.'/libmgr.php');
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
+
clsLibMgr::Add('strings', KFP_LIB.'/strings.php',__FILE__,__LINE__);
define('KSI_ITEM_TOTAL' ,301);
+
clsLibMgr::Add('string.tplt', KFP_LIB.'/StringTemplate.php',__FILE__,__LINE__);
define('KSI_PER_ITEM_TOTAL' ,302);
+
clsLibMgr::Add('tree', KFP_LIB.'/tree.php',__FILE__,__LINE__);
define('KSI_PER_PKG_TOTAL' ,303);
+
clsLibMgr::Add('vbz.store', KFP_LIB_VBZ.'/store.php',__FILE__,__LINE__);
 
+
  clsLibMgr::AddClass('clsVbzPage', 'vbz.store');
// ORDER MESSAGE TYPES
+
clsLibMgr::Add('vbz.cart', KFP_LIB_VBZ.'/cart.php',__FILE__,__LINE__);
// these reflect the values in the ord_msg_media table
+
  clsLibMgr::AddClass('clsPageCart', 'vbz.cart');
define('KSI_ORD_MSG_INSTRUC', 1); // Instructions in submitted order
+
  clsLibMgr::AddClass('clsShopCarts','vbz.cart');
define('KSI_ORD_MSG_PKSLIP', 2); // Packing slip
+
clsLibMgr::Add('vbz.order', KFP_LIB_VBZ.'/orders.php',__FILE__,__LINE__);
define('KSI_ORD_MSG_EMAIL', 3); // Email
+
  clsLibMgr::AddClass('clsOrders', 'vbz.order');
define('KSI_ORD_MSG_PHONE', 4); // Phone call
+
clsLibMgr::Load('strings',__FILE__,__LINE__);
define('KSI_ORD_MSG_MAIL', 5); // Snail mail
+
clsLibMgr::Load('string.tplt',__FILE__,__LINE__);
define('KSI_ORD_MSG_FAX', 6); // Faxed message
+
clsLibMgr::Load('tree',__FILE__,__LINE__);
define('KSI_ORD_MSG_LABEL', 7); // Shipping label (for delivery instructions)
+
clsLibMgr::Load('vbz.store',__FILE__,__LINE__);
define('KSI_ORD_MSG_INT', 8); // internal use - stored, not sent
+
//   clsLibMgr::Load('vbz.cart',__FILE__,__LINE__);
 
 
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');
 
define('KS_VBZCART_SESSION_KEY','vbzcart_key');
Line 184: Line 58:
 
define('KSQ_PAGE_SHIP','ship'); // shipping page
 
define('KSQ_PAGE_SHIP','ship'); // shipping page
 
define('KSQ_PAGE_PAY','pay'); // payment 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_CONF','conf'); // customer confirmation of order
 
define('KSQ_PAGE_RCPT','rcpt'); // order receipt
 
define('KSQ_PAGE_RCPT','rcpt'); // order receipt
Line 192: Line 67:
 
  database class with creators for shop classes
 
  database class with creators for shop classes
 
*/
 
*/
abstract class clsPageShop extends clsPage {
+
class clsVbzData_Shop extends clsVbzData {
     public function Sessions() {
+
     public function Sessions($id=NULL) {
return $this->Make('clsShopSessions');
+
return $this->Make('clsSessions_StoreUI',$id);
 
     }
 
     }
     public function Clients() {
+
     public function Clients($id=NULL) {
return $this->Make('clsShopClients');
+
return $this->Make('clsShopClients',$id);
 
     }
 
     }
     public function Carts() {
+
     public function Carts($id=NULL) {
return $this->Make('clsShopCarts');
+
return $this->Make('clsShopCarts',$id);
 
     }
 
     }
     public function CartLines() {
+
     public function CartLines($id=NULL) {
return $this->Make('clsShopCartLines');
+
return $this->Make('clsShopCartLines',$id);
 
     }
 
     }
 
     public function CartLog() {
 
     public function CartLog() {
 
return $this->Make('clsShopCartLog');
 
return $this->Make('clsShopCartLog');
 
     }
 
     }
     public function Orders() {
+
     public function Orders($id=NULL) {
return $this->Make('clsOrders');
+
return $this->Make('clsOrders',$id);
 
     }
 
     }
     public function OrdLines() {
+
     public function OrdLines($id=NULL) {
return $this->Make('clsOrderLines');
+
return $this->Make('clsOrderLines',$id);
 
     }
 
     }
 
/*
 
/*
Line 219: Line 94:
 
     }
 
     }
 
*/
 
*/
     public function OrdMsgs() {
+
     public function OrdMsgs($id=NULL) {
return $this->Make('clsOrderMsgs');
+
return $this->Make('clsOrderMsgs',$id);
 
     }
 
     }
 
/*
 
/*
Line 244: Line 119:
 
}
 
}
  
class clsPageCart extends clsPageShop {
+
/*==================
    protected $objSess;
+
   CLASS: clsShipZone
    protected $objCart;
+
   PURPOSE: shipping zone functions
 
+
   USAGE: Customize the isDomestic() function if you're shipping from somewhere other than the US
// 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 = ''; //'<dt><b>Cat #</b>: '.$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();
 
    }
 
    public function Save() {
 
$acts = NULL;
 
if ($this->AccessCount() == 0) {
 
    $acts = $this->ScriptStart();
 
 
 
    $acts->Add($this->SaveThis(),'this');
 
    $acts->Add($this->SaveSubs(),'subs');
 
    $acts->Add($this->SavePost($acts),'post');
 
 
 
    if ($acts->IsEmpty()) {
 
$acts = NULL;
 
    }
 
}
 
$this->IncCount();
 
$this->actSave = $acts;
 
return $acts;
 
    }
 
    abstract protected function SaveThis();
 
    protected function SavePost(Script_Script $iScript) {}
 
 
 
    /*----
 
      ACTION: Save sub-nodes
 
    */
 
    protected function SaveSubs() {
 
if ($this->HasNodes()) {
 
    $acts = new Script_Script();
 
    $ar = $this->Nodes();
 
    foreach ($ar as $name => $obj) {
 
$acts->Add($obj->Save(),$name);
 
    }
 
} 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() {} // nothing to save here; only saves sub-nodes
 
}
 
class clsContactFolder_reference extends clsContactFolder {
 
    protected function SaveSubs() {} // 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() {
 
$this->ResetCount();
 
return parent::Save();
 
    }
 
}
 
/*====
 
  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 '<b>Internal error</b>: argument $iIndex is an object of class '.get_class($iIndex).', not a scalar.<br>';
 
    throw new exception('Unexpected argument type.');
 
}
 
$this->objCart = $iCart;
 
$this->intIndex = $iIndex;
 
$this->strCtrlName = $iCtrlName;
 
    }
 
    protected function SaveThis() { } // 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() {
 
return $this->Name()->Save();
 
    }
 
 
 
    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 = '<b>'.$strCountry.'</b>';
 
    $htZone = '';
 
} else {
 
    $htCountry = '<input name="'.$ksCountry.'" value="'.$strCountry.'" size=20>';
 
    $htShipCombo = $this->htShipCombo;
 
    $htBtnRefresh = '<input type=submit name="update" value="Update Form">';
 
    $htZone = " - change shippping zone: $htShipCombo $htBtnRefresh";
 
}
 
 
 
if ($this->doFixedName) {
 
    $out = <<<__END__
 
<tr><td align=right valign=middle>Name:</td>
 
<td><b>$strName</b></td>
 
</tr>
 
__END__;
 
} else {
 
    $out = <<<__END__
 
<tr><td align=right valign=middle>Name: </td>
 
<td><input name="$ksName" value="$strName" size=50></td>
 
</tr>
 
__END__;
 
}
 
$out .= $this->htmlBeforeAddress;
 
if ($this->doFixed) {
 
    $out .= <<<__END__
 
<tr><td align=right valign=middle>Street Address<br>or P.O. Box:</td>
 
<td><b>$strStreet</b></td>
 
</tr>
 
<tr><td align=right valign=middle>City:</td><td><b>$strCity</b></td></tr>
 
<tr><td align=right valign=middle>$strStateLabel:</td><td><b>$strState</b></td></tr>
 
<tr><td align=right valign=middle>$strZipLabel:</td><td><b>$strZip</b></td></tr>
 
<tr><td align=right valign=middle>Country:</td><td><b>$htCountry<b>$htZone</td></tr>
 
__END__;
 
} else {
 
    $out .= <<<__END__
 
<tr><td align=right valign=middle>Street Address<br>or P.O. Box:</td>
 
<td><textarea name="$ksStreet" cols=50 rows=3>$strStreet</textarea></td>
 
</tr>
 
<tr><td align=right valign=middle>City: </td><td><input name="$ksCity" value="$strCity" size=20></td></tr>
 
<tr><td align=right valign=middle>$strStateLabel: </td><td><input name="$ksState" value="$strState" size=$lenState>$strStateAfter</td></tr>
 
<tr><td align=right valign=middle>$strZipLabel: </td><td><input name="$ksZip" value="$strZip" size=11></td></tr>
 
<tr><td align=right valign=middle>Country: </td><td>$htCountry - change shippping zone: $htShipCombo $htBtnRefresh</td></tr>
 
__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__
 
<tr><td align=right valign=middle>Email:</td><td><b>$strEmail</b></td></tr>
 
<tr><td align=right valign=middle>Phone:</td><td><b>$strPhone</b></td></tr>
 
__END__;
 
} else {
 
    $out = <<<__END__
 
<tr><td align=right valign=middle>Email:</td>
 
<td><input name="$ksEmail" value="$strEmail" size=30> {$hrefForSpam}anti-spam policy</a>
 
</td>
 
</tr>
 
<tr><td align=right valign=middle>Phone:</td>
 
<td><input name="$ksPhone" value="$strPhone" size=20> (optional)</td>
 
</tr>
 
__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 .= '<br>'.$this->Addr()->AsText("\n<br>");
 
return $out;
 
    }
 
}
 
/*----
 
 
   RULES:
 
   RULES:
     * a Person must have two mailing addresses: one for shipping, one for payment
+
     * If a country's code isn't found in arDesc, it defaults to International
    * these addresses may be the same
+
      ...there's got to be a better way to do this...
    * 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 {
+
class clsShipZone {
/*
+
     static private $arDesc = array(
    public $Contact;
+
      'CA' => 'Canada',
    public $Payment;
+
      'US' => 'United States',
*/
+
      'INT' => 'International',
    protected $strName; // for forms
+
      );
     private $strDescr; // for text messages
+
     // per-item adjustment factors
 
+
     static private $arItmFactors = array(
    public function __construct($iName,$iDescr) {
+
'US' => 1.0,
$this->strName = $iName;
+
'CA' => 2.0,
$this->strDescr = $iDescr;
+
'INT' => 4.0,
    }
+
      );
    public function Descr($iDescr=NULL) {
+
     // per-package adjustment factors
if (!is_null($iDescr)) {
+
     static private $arPkgFactors = array( // there's got to be a better way to do this...
    $this->strDescr = $iDescr;
+
'US' => 1.0,
}
+
'CA' => 2.0,
return $this->strDescr;
+
'INT' => 4.0,
    }
+
       );
    public function CtrlName() {
+
    static private $arCountryCodes = array(
return $this->strName;
+
'united states' => 'US',
     }
+
'canada' => 'CA',
     public function HasContact() {
+
'australia' => 'AU',
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
+
     private $strAbbr;
    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:
 
<form> 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 = '<b>Creating</b> 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 = '<b>Using</b> 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:<pre>'.print_r($arXl,TRUE).'</pre>';
 
echo 'ord scratch:<pre>'.print_r($actOrdScratch->Values(),TRUE).'</pre>';
 
$acts->Add(new Script_Copy_Named($actOrdScratch,$iOrder,$arXl),'order.update.'.$strFormName);
 
//echo '#1:<pre>'.print_r($iOrder->Values(),TRUE).'</pre>';
 
 
// 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:<pre>'.print_r($iOrder->Values(),TRUE).'</pre>';
 
    $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:<pre>'.print_r($arXl,TRUE).'</pre>';
 
echo 'ord scratch:<pre>'.print_r($actOrdScratch->Values(),TRUE).'</pre>';
 
$acts->Add(new Script_Copy_Named($actOrdScratch,$iOrder,$arXl),'order.update.'.$strFormName);
 
//echo '#1:<pre>'.print_r($iOrder->Values(),TRUE).'</pre>';
 
   
 
// 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=<b>'.$type.'</b> idName=<b>'.$idName.'</b> idAddr=<b>'.$idAddr.'</b>';
 
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) {
 
     public function Abbr($iAbbr=NULL) {
 
 
if (!is_null($iAbbr)) {
 
if (!is_null($iAbbr)) {
 
    $this->strAbbr = $iAbbr;
 
    $this->strAbbr = $iAbbr;
Line 1,369: Line 163:
 
return $this->strAbbr;
 
return $this->strAbbr;
 
     }
 
     }
     public function Text() {
+
     public function Set_fromName($iName) {
global $listShipListDesc;
+
$strLC = strtolower($iName);
 
+
if (array_key_exists($strLC,self::$arCountryCodes)) {
return $listShipListDesc[$this->Abbr()];
+
    $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() {
 
     public function hasState() {
 
switch ($this->Abbr()) {
 
switch ($this->Abbr()) {
 +
  case 'AU': return TRUE; break;
 +
  case 'CA': return TRUE; break;
 
  case 'US': return TRUE; break;
 
  case 'US': return TRUE; break;
  case 'CA': return TRUE; break;
 
 
  default: return FALSE; break;
 
  default: return FALSE; break;
 
}
 
}
Line 1,384: Line 186:
 
     public function StateLabel() {
 
     public function StateLabel() {
 
switch ($this->Abbr()) {
 
switch ($this->Abbr()) {
 +
  case 'AU': return 'State/Territory'; break;
 +
  case 'CA': return 'Province'; break;
 
  case 'US': return 'State'; break;
 
  case 'US': return 'State'; break;
  case 'CA': return 'Province'; break;
 
 
  default: return 'County/Province'; break;
 
  default: return 'County/Province'; break;
 
}
 
}
Line 1,406: Line 209:
 
     }
 
     }
 
     public function ComboBox() {
 
     public function ComboBox() {
global $listShipListDesc;
 
 
 
$strZoneCode = $this->Abbr();
 
$strZoneCode = $this->Abbr();
 
$out = '<select name="ship-zone">';
 
$out = '<select name="ship-zone">';
foreach ($listShipListDesc as $key => $descr) {
+
foreach (self::$arDesc as $key => $descr) {
 
//$dest (keys(%listShipListDesc)) {
 
//$dest (keys(%listShipListDesc)) {
 
$strZoneDesc = $descr;
 
$strZoneDesc = $descr;
Line 1,422: Line 223:
 
}
 
}
 
$out .= '</select>';
 
$out .= '</select>';
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".'<!-- Cart ID='.$this->KeyValue().' | Session ID='.$this->ID_Sess.' -->';
 
$out .= "\n<center><table class=border><tr><td><table class=cart><tr><td align=center valign=middle>";
 
$out .= "\n<form method=post action='./'>";
 
$out .= "\n<table class=cart-data>\n";
 
return $out;
 
    }
 
    public function RenderFtr() {
 
return KHT_CART_FTR;
 
    }
 
    /*----
 
      ACTION: Render the receipt in HTML
 
Shows the order as generated from *cart* data, not what's in the order record.
 
...except for the order number.
 
      HISTORY:
 
2011-03-27 adapting this from clsOrder::RenderReceipt()
 
    */
 
    public function RenderReceipt() {
 
$out = NULL;
 
 
$objCart = $this;
 
$objOrd = $this->OrderObj();
 
 
// load contact data
 
assert('is_object($objOrd);');
 
if (($objOrd->ID == 0) || ($objCart->ID == 0)) {
 
    throw new exception('Receipt has missing object: Order ID='.$this->Value('ID_Order').', Cart ID='.$objCart->KeyValue());
 
}
 
$objCart->GetDetailObjs();
 
$objPay = $objCart->PersonCustObj()->Payment();
 
$objAddrCard = $objCart->AddrCardObj();
 
// the next line is a kluge which only works as long as payment is always ccard
 
// it's also not clear why GetDetailObjs() isn't loading it properly
 
$objPay->Node('addr', $objAddrCard);
 
 
$arVars = array(
 
  'ord.num' => $objOrd->Number,
 
  'timestamp' => date(KF_RCPT_TIMESTAMP),
 
  'cart.id' => $objCart->ID,
 
  'cart.detail' => $objCart->RenderConfirm(),
 
  'ship.name' => $objCart->AddrShipObj()->Name()->Value(),
 
  'ship.addr' => $objCart->AddrShipObj()->AsText("\n<br>"),
 
  'pay.name' => $objPay->Addr()->Name()->Value(),
 
  'pay.spec' => $objPay->SafeDisplay(),
 
  'email.short' => 'orders-'.date('Y').'@vbz.net'
 
  );
 
$objStrTplt = new clsStringTemplate_array(NULL,NULL,$arVars);
 
$objStrTplt->MarkedValue(KHT_RCPT_TPLT);
 
$out = '<!-- ORDER ID: '.$objOrd->ID.' / CART ID from order: '.$objCart->ID.' -->';
 
$out .= $objStrTplt->Replace();
 
return $out;
 
    }
 
    /*-----
 
      ACTION: Renders the order contents as plaintext, suitable for emailing
 
    */
 
    public function RenderOrder_Text() {
 
/* NOT USED HERE
 
$isShipCard = $this->DataItem(KSI_SHIP_IS_CARD);
 
$isShipSelf = $this->DataItem(KSI_SHIP_TO_SELF);
 
*/
 
// copy any needed constants over to variables for parsing:
 
$ksShipMsg = KSF_SHIP_MESSAGE;
 
$ksfCustCardNum = KSF_CUST_CARD_NUM;
 
$ksfCustCardExp = KSF_CUST_CARD_EXP;
 
 
// get non-address field data:
 
$strCardNum = $this->DataItem(KSI_CUST_CARD_NUM);
 
$strCardExp = $this->DataItem(KSI_CUST_CARD_EXP);
 
$strCustShipMsg = $this->DataItem(KSI_SHIP_MESSAGE);
 
$ftCustShipMsg = wordwrap($strCustShipMsg);
 
 
$out = '';
 
$out .= "ITEMS ORDERED:\n";
 
 
$out .= $this->RenderCore_Text();
 
 
$this->doFixedCard = TRUE;
 
$this->doFixedSelf = TRUE;
 
$this->doFixedName = TRUE;
 
$this->htmlBeforeAddress = '';
 
$this->htmlBeforeContact = '';
 
 
$out .= "\n\nSHIP TO:\n";
 
$out .= '  '.$this->AddrShipObj()->Name()->Value()."\n";
 
$out .= '  '.$this->AddrShipObj()->AsText("\n  ");
 
$out .= "\n";
 
$out .= "\n  Email: ".$this->ContDestObj()->Email()->Value();
 
$out .= "\n  Phone: ".$this->ContDestObj()->Phone()->Value();
 
 
$out .= "\n\n  ";
 
if (empty($strCustShipMsg)) {
 
    $out .= "(No special instructions)";
 
} else {
 
    $out .= "Special Instructions:\n$ftCustShipMsg";
 
}
 
$out .= "\n\nPAYMENT:\n  ".clsCustCards::SafeDescr_Long($strCardNum,$strCardExp)."\n";
 
$out .= '  '.$this->AddrCardObj()->Name()->Value()."\n";
 
$out .= '  '.$this->AddrCardObj()->AsText("\n  ");
 
$out .= "\n";
 
$out .= "\n  Email: ".$this->ContCustObj()->Email()->Value();
 
$out .= "\n  Phone: ".$this->ContCustObj()->Phone()->Value();
 
return $out;
 
    }
 
    public function RenderCore($iAsForm) {
 
$strZone = $this->objShipZone->Abbr();
 
$shipMinCost = 0;
 
 
$out = '<tr>'
 
  .'<th><big>cat #</big></th>'
 
  .'<th><big>description</big></th>'
 
  .'<th>price<br>each</th>'
 
  .'<th><small>per-item<br>s/h ea.</small></th>'
 
  .'<th>qty.</th>'
 
  .'<th><small>purchase<br>line total</small></th>'
 
  .'<th><small>per-item<br>s/h line total</small></th>'
 
  .'<th>totals</th>'
 
  .'<th><small>pkg s/h<br>min.</small></th>'
 
  .'</tr>';
 
 
$rsLine = $this->objLines;
 
while ($rsLine->NextRow()) {
 
    if ($iAsForm) {
 
$out .= $rsLine->RenderForm($this);
 
    } else {
 
$out .= $rsLine->RenderHtml($this);
 
    }
 
    if ($shipMinCost < $rsLine->ShipPkgDest) {
 
    $shipMinCost = $rsLine->ShipPkgDest;
 
    }
 
    $intQty = $rsLine->Qty;
 
    $this->CostTotalItem += $rsLine->CostItemQty;
 
    $this->CostTotalShip += $rsLine->CostShipQty;
 
}
 
// save official totals for order creation:
 
// TO DO: are CostTotalItem and CostTotalShip referenced anywhere else? Make them local if not.
 
// But if they are, then why isn't shipMinCost also a field?
 
$this->DataItem(KSI_ITEM_TOTAL,$this->CostTotalItem);
 
$this->DataItem(KSI_PER_ITEM_TOTAL,$this->CostTotalShip);
 
$this->DataItem(KSI_PER_PKG_TOTAL,$shipMinCost);
 
 
$strTotalMerch = FormatMoney($this->CostTotalItem);
 
$strItemsShip = FormatMoney($this->CostTotalShip);
 
$strTotalItems = FormatMoney($this->CostTotalItem + $this->CostTotalShip);
 
$strShipZone = $this->objShipZone->Text();
 
$strShipDesc = $strShipZone.' s/h package cost:';
 
$strShipPkg = FormatMoney($shipMinCost);
 
$strTotalDesc = 'order total if shipping to '.$strShipZone.':';
 
$strOrdTotal = FormatMoney($this->CostTotalItem + $this->CostTotalShip + $shipMinCost);
 
 
$objSess = $this->Session();
 
 
if ($iAsForm) {
 
    $htDelAll = '<span class=text-btn>[<a href="?action=delcart" title="remove all items from cart">remove all</a>]</span>';
 
    $htFirstTot = "<td align=left>$htDelAll</td><td align=right class=total-desc colspan=4>totals:</td>";
 
    $htZoneCombo = 'Shipping destination: '.$this->objShipZone->ComboBox();
 
} else {
 
    $htFirstTot = '<td align=right class=total-desc colspan=5>totals:</td>';
 
    $htZoneCombo = 'Shipping costs shown assume shipment to <b>'.$this->objShipZone->Text().'</b> address.';
 
}
 
$out .= <<<__END__
 
<tr>$htFirstTot
 
<td align=right class=total-amount>$strTotalMerch</td>
 
<td align=right class=total-amount>$strItemsShip</td>
 
<td align=right class=total-amount>$strTotalItems</td>
 
<td align=right>&dArr;</td>
 
</tr>
 
<tr>
 
<td align=right  class=total-desc colspan=7>$strShipDesc</td>
 
<td align=right  class=total-amount>$strShipPkg</td>
 
<td align=right>&crarr;</td>
 
</tr>
 
<tr>
 
<td align=right  class=total-desc colspan=7>$strTotalDesc</td>
 
<td align=right  class=total-final>$strOrdTotal</td>
 
</tr>
 
<tr><td colspan=6>$htZoneCombo</td></tr>
 
__END__;
 
$this->LogEvent('disp','displaying cart, zone '.$this->objShipZone->Abbr().' total $'.$strOrdTotal);
 
return $out;
 
    }
 
    /*-----
 
      RETURNS: The contents of the cart as text. Includes column headers and totals.
 
      USED BY: does anything actually use this, or was it intended for the email confirmation?
 
    */
 
    public function RenderCore_Text() {
 
$abbrShipZone = $this->DataItem(KSI_SHIP_ZONE);
 
$this->objShipZone->Abbr($abbrShipZone);
 
 
$shipMinCost = 0;
 
 
$strLineFmt = '%-16s |%6.2f |%6.2f |%4d |%7.2f |%10.2f |%13.2f';
 
 
if ($this->HasRows()) {
 
    $hdr = "\n".sprintf('%-17s|%6s|%5s|%5s|%7s|%10s|%13s'
 
      ,'cat #'
 
      ,' $ ea. '
 
      ,' $ s/h '
 
      ,' qty '
 
      ,' $ sale '
 
      ,' $ s/h tot '
 
      ,' $ LINE TOTAL');
 
    $out = $hdr;
 
    $out .= "\n".str_repeat('-',strlen($hdr));
 
    $objLines = $this->GetLines();
 
    $dlrSaleTot = 0; // total sale before shipping
 
    $dlrPItmTot = 0; // per-item shipping total
 
    $dlrPPkgMax = 0; // per-pkg shipping total
 
    while ($objLines->NextRow()) {
 
$objItem = $objLines->Item();
 
$dlrShipItm = $objItem->ShipPriceItem($abbrShipZone);
 
$dlrShipPkg = $objItem->ShipPricePkg($abbrShipZone);
 
 
$out .= $objLines->RenderText($this,$strLineFmt);
 
if ($dlrPPkgMax < $dlrShipPkg) {
 
    $dlrPPkgMax = $dlrShipPkg;
 
}
 
$intQty = $objLines->Qty;
 
$dlrSaleTot += $objLines->PriceItem;
 
$dlrPItmTot += $dlrShipItm*$intQty;
 
    }
 
    $out .= "\n".str_repeat('=',strlen($hdr));
 
 
    $ftTotalMerch = sprintf('%6.2f',FormatMoney($dlrSaleTot));
 
    $ftItemsShip = sprintf('%6.2f',FormatMoney($dlrPItmTot));
 
    $ftTotalItems = sprintf('%6.2f',FormatMoney($dlrSaleTot + $dlrPItmTot));
 
    $ftShipPkg = sprintf('%6.2f',FormatMoney($dlrPPkgMax));
 
    //$ftTotalDesc = 'order total for shipping to '.$ftShipZone.':';
 
    $ftOrdTotal = sprintf('%6.2f',FormatMoney($dlrSaleTot + $dlrPItmTot + $dlrPPkgMax));
 
 
// these items don't depend on cart contents, but there's no point in calculating them if the cart is empty:
 
$ftShipZone = $this->objShipZone->Text();
 
$ftShipDesc = $ftShipZone.' s/h package cost:';
 
 
//$objSess = $this->Session();
 
 
$ftZone = $this->objShipZone->Text();
 
//$ftZone = $this->objShipZone->Abbr();
 
//$ftZone = $abbrShipZone;
 
 
$out .= "\n"
 
  ."\n *          Sale: $ftTotalMerch"
 
  ."\n * S/H -"
 
  ."\n  * per item sum: $ftItemsShip"
 
  ."\n  * per  package: $ftShipPkg"
 
  ."\n========================"
 
  ."\n==== FINAL TOTAL: $ftOrdTotal"
 
  ."\n\nShipping Zone: $ftZone";
 
} else {
 
    $out = "\nSorry, we seem to have goofed: this cart appears to have no items in it.";
 
    $out .= "\nThe webmaster is being alerted to the problem.";
 
}
 
return $out;
 
    }
 
    public function Render() {
 
// return rendering of current contents of cart
 
$ok = FALSE;
 
if ($this->ID) {
 
    if ($this->HasLines()) {
 
$ok = TRUE;
 
    }
 
}
 
 
if ($ok) {
 
# get information for that destination type:
 
    $out = $this->RenderHdr();
 
    $out .= $this->RenderCore(TRUE);
 
    $out .= $this->RenderFtr();
 
} else {
 
    if ($this->IsCreated()) {
 
if (is_null($this->objLines)) {
 
    $out = "<font size=4>Internal error - cart data not available!</font>";
 
    $this->LogEvent('disp',"can't display cart - no data!");
 
    $this->objDB->Events()->LogEvent('cart.render','','cart data unavailable','cdna',TRUE,TRUE);
 
} else {
 
    $out = "<font size=4>Your cart is empty.</font>";
 
    $this->LogEvent('disp','displaying cart - empty; zone '.$this->objShipZone->Abbr());
 
}
 
    } else {
 
$out = "<font size=4>You have not put anything in your cart yet.</font>";
 
$this->LogEvent('disp','displaying cart - nothing yet; zone '.$this->objShipZone->Abbr());
 
    }
 
}
 
 
return $out;
 
return $out;
 
     }
 
     }
 
     /*----
 
     /*----
    PURPOSE: Render cart for order confirmation page (read-only, no form controls)
+
      RETURNS: per-item price factor for the current shipping zone
 
     */
 
     */
     public function RenderConfirm() {
+
     protected function PerItemFactor() {
if ($this->HasLines()) {
+
echo 'CODE=['.$this->Abbr().'] ITEM FACTOR=['.self::$arItmFactors[$this->Abbr()].']<br>';
    $out = $this->RenderCore(FALSE);
+
return self::$arItmFactors[$this->Abbr()];
} else {
 
    // log error - you shouldn't be able to get to this point with an empty cart
 
    $txtParams = 'Cart ID='.$this->ID.' Order ID='.$this->ID_Order;
 
    $this->objDB->LogEvent('cart.renderconf',$txtParams,'cart empty at confirmation','cec',TRUE,TRUE); // also sends email alert
 
    $out = '<font color=red>INTERNAL ERROR</font>: cart data has become separated from browser. The webmaster has been notified.';
 
}
 
return $out;
 
    }
 
/*==========
 
  CUSTOMER DATA AMALGAMATION
 
*/
 
    public function AddrShipObj() {
 
if (empty($this->objAddrShip)) {
 
    $arFields = array(
 
      'name' => $this->SpawnName(KSI_ADDR_SHIP_NAME ,KSF_ADDR_SHIP_NAME),
 
      'street' => new clsCartField($this, KSI_ADDR_SHIP_STREET, KSF_ADDR_SHIP_STREET),
 
      'city' => new clsCartField($this, KSI_ADDR_SHIP_CITY ,KSF_ADDR_SHIP_CITY),
 
      'state' => new clsCartField($this, KSI_ADDR_SHIP_STATE ,KSF_ADDR_SHIP_STATE),
 
      'zip' => new clsCartField($this, KSI_ADDR_SHIP_ZIP ,KSF_ADDR_SHIP_ZIP),
 
      'country' => new clsCartField($this, KSI_ADDR_SHIP_COUNTRY,KSF_ADDR_SHIP_COUNTRY),
 
      'instruc' => new clsCartField($this, KSI_SHIP_MESSAGE ,KSF_SHIP_MESSAGE)
 
      );
 
    $this->objAddrShip = $this->SpawnAddress($arFields);
 
}
 
return $this->objAddrShip;
 
    }
 
    public function AddrCardObj() {
 
if (empty($this->objAddrCard)) {
 
    $arFields = array(
 
      'name' => $this->SpawnName(KSI_ADDR_CARD_NAME ,KSF_CUST_CARD_NAME),
 
      'street' => new clsCartField($this, KSI_ADDR_CARD_STREET ,KSF_CUST_CARD_STREET),
 
      'city' => new clsCartField($this, KSI_ADDR_CARD_CITY ,KSF_CUST_CARD_CITY),
 
      'state' => new clsCartField($this, KSI_ADDR_CARD_STATE ,KSF_CUST_CARD_STATE),
 
      'zip' => new clsCartField($this, KSI_ADDR_CARD_ZIP ,KSF_CUST_CARD_ZIP),
 
      'country' => new clsCartField($this, KSI_ADDR_CARD_COUNTRY,KSF_CUST_CARD_COUNTRY),
 
      'instruc' => new clsCartField($this, NULL ,NULL)
 
      );
 
    $this->objAddrCard = $this->SpawnAddress($arFields);
 
}
 
return $this->objAddrCard;
 
    }
 
    public function ContDestObj() {
 
if (empty($this->objContDest)) {
 
    $arFields = array(
 
      'email' => $this->SpawnEmail(KSI_CUST_SHIP_EMAIL ,KSF_CUST_SHIP_EMAIL),
 
      'phone' => $this->SpawnPhone(KSI_CUST_SHIP_PHONE ,KSF_CUST_SHIP_PHONE)
 
      );
 
    $this->objContDest = $this->SpawnContact($arFields); // blank object for rendering
 
}
 
return $this->objContDest;
 
    }
 
    public function ContCustObj_raw() {
 
if (empty($this->objContCust)) {
 
    $arFields = array(
 
      'email' => $this->SpawnEmail(KSI_CUST_PAY_EMAIL ,KSF_CUST_PAY_EMAIL),
 
      'phone' => $this->SpawnPhone(KSI_CUST_PAY_PHONE ,KSF_CUST_PAY_PHONE)
 
      );
 
    $this->objContCust = $this->SpawnContact($arFields);
 
}
 
return $this->objContCust;
 
    }
 
    /*----
 
      RETURNS: Contact information for the customer. If the "shipping to myself" box was checked,
 
then it returns the shipping contact information instead, since this is presumed to be the
 
customer's contact information as well.
 
    */
 
    public function ContCustObj() {
 
//$isShipCard = $this->DataItem(KSI_SHIP_IS_CARD);
 
$isShipSelf = $this->DataItem(KSI_SHIP_TO_SELF);
 
if ($isShipSelf) {
 
    return $this->ContDestObj();
 
} else {
 
    return $this->ContCustObj_raw();
 
}
 
    }
 
    /*----
 
      METHOD: SpawnPerson()
 
      ACTION: creates clsPerson object or descendant
 
This lets us stub off admin functionality here and extend it later.
 
      HISTORY:
 
2011-11-29 created
 
    */
 
    public function SpawnPerson($iName,$iDescr) {
 
return new clsPerson($iName,$iDescr);
 
    }
 
    /*----
 
      METHOD: SpawnPayment()
 
      ACTION: creates clsPayment object or descendant
 
This lets us stub off admin functionality here and extend it later.
 
      HISTORY:
 
2011-11-29 created
 
    */
 
    public function SpawnPayment($iNodes=NULL) {
 
return new clsPayment($iNodes);
 
    }
 
    /*----
 
      METHOD: SpawnAddress()
 
      ACTION: creates clsCartAddr object or descendant
 
This lets us stub off admin functionality here and extend it later.
 
      HISTORY:
 
2011-11-29 created
 
    */
 
    public function SpawnAddress($iNodes=NULL) {
 
return new clsCartAddr($iNodes);
 
    }
 
    /*----
 
      METHOD: SpawnContact()
 
      ACTION: creates clsCartContact object or descendant
 
This lets us stub off admin functionality here and extend it later.
 
The extra functionality may not actually be needed for this, but I went and created it anyway.
 
  (2011-12-15: yes, it was needed.)
 
      HISTORY:
 
2011-11-29 created
 
    */
 
    public function SpawnContact($iNodes=NULL) {
 
return new clsCartContact($iNodes);
 
    }
 
    /*----
 
      METHOD: SpawnEmail()
 
      ACTION: creates object for handling cart email address data
 
This lets us stub off admin functionality here and extend it later.
 
      HISTORY:
 
2011-11-29 created
 
    */
 
    public function SpawnEmail($iIndex, $iCtrlName) {
 
return new clsCartField($this, $iIndex, $iCtrlName);
 
    }
 
    /*----
 
      METHOD: SpawnPhone()
 
      ACTION: creates object for handling cart phone number data
 
This lets us stub off admin functionality here and extend it later.
 
      HISTORY:
 
2011-11-29 created
 
    */
 
    public function SpawnPhone($iIndex, $iCtrlName) {
 
return new clsCartField($this, $iIndex, $iCtrlName);
 
    }
 
    /*----
 
      METHOD: SpawnName()
 
      ACTION: creates object for handling person-name data
 
This lets us stub off admin functionality here and extend it later.
 
      HISTORY:
 
2011-11-30 created
 
    */
 
    public function SpawnName($iIndex, $iCtrlName) {
 
return new clsCartField($this, $iIndex, $iCtrlName);
 
 
     }
 
     }
 
     /*----
 
     /*----
       METHOD: clsShopCart->PersonCustObj()
+
       RETURNS: per-package price factor for the current shipping zone
      USED BY: clsShopCart->GetDetailObjs(), clsOrder->RenderReceipt()
 
      HISTORY:
 
2010-09-12 Extracted from clsShopCart->GetDetailObjs() so it can be used by clsOrder->RenderReceipt()
 
2011-11-29 Now using SpawnPayment() and SpawnPerson() instead of creating directly with "new"
 
 
     */
 
     */
     public function PersonCustObj() {
+
     protected function PerPkgFactor() {
if (empty($this->objCust)) {
+
return self::$arPkgFactors[$this->Abbr()];
    $arFields = array(
 
      'num' => new clsCartField($this, KSI_CUST_CARD_NUM, KSF_CUST_CARD_NUM),
 
      'exp' => new clsCartField($this, KSI_CUST_CARD_EXP, KSF_CUST_CARD_EXP)
 
      );
 
    $objPayment = $this->SpawnPayment($arFields);
 
    $this->objCust = $this->SpawnPerson('cust','buyer');
 
    $this->objCust->Node('payment', $objPayment); // the buyer always has the credit card
 
}
 
return $this->objCust;
 
    }
 
    /*-----
 
      METHOD: clsShopCart->GetDetailObjs()
 
      FUTURE: rename to GetDataTree()
 
      HISTORY:
 
2010-09-12 borrowed (to be moved?) from clsPageCkout
 
2011-11-27 the version in clsPageCkout has now been commented out
 
2011-11-29 using SpawnPerson() instead of new clsPerson()
 
    */
 
    public function GetDetailObjs() {
 
if (!$this->hasDetails) {
 
    $objAddrShip = $this->AddrShipObj();
 
    $objAddrCard = $this->AddrCardObj();
 
    $objContDest = $this->ContDestObj();
 
    $objContCust = $this->ContCustObj();
 
 
 
    $objDataTree = new clsContactRoot($this->Engine());
 
/*
 
    $objDataRef = new clsContactFolder_reference();
 
    $objDataTree->Node('reference data',$objDataRef);
 
    $objDataRef->Node('addr.ship',$objAddrShip);
 
    $objDataRef->Node('addr.card',$objAddrCard);
 
    $objDataRef->Node('cont.dest',$objContDest);
 
    $objDataRef->Node('cont.cust',$objContCust);
 
*/
 
    $this->objShip = $this->SpawnPerson('ship','recipient');
 
    $this->objCust = $this->PersonCustObj();
 
 
 
    $objDataTree->Node('person.ship',$this->objShip);
 
    $objDataTree->Node('person.cust',$this->objCust);
 
 
 
    $this->objDataTree = $objDataTree;
 
 
 
    $objPayment = $this->objCust->Payment();
 
 
 
    $this->objShip->Node('contact', $objContDest); // shipping information is always specified
 
 
 
    $objContDest->Addr = $objAddrShip; // shipping address
 
 
 
    $this->custShipIsCard = $this->DataItem(KSI_SHIP_IS_CARD);
 
    $this->custShipToSelf = $this->DataItem(KSI_SHIP_TO_SELF);
 
 
 
    if ($this->custShipIsCard) {
 
$objPayment->Node('addr', $objAddrShip); // use shipping address for card
 
    } else {
 
$objPayment->Node('addr', $objAddrCard); // use separate address for card
 
    }
 
 
 
    $this->objShip->Node('name', $objAddrShip->Name());
 
 
 
    if ($this->custShipToSelf) {
 
// don't use separate person data; re-use buyer contact info plus shipping address
 
$this->objShip->Node('payment', $objPayment); // the only buyer field the recipient doesn't have
 
$this->objCust = $this->objShip;
 
//$objContDest->Node('addr', $objAddrShip); // shipping address
 
$this->objShip->Node('contact', $objContDest);
 
$objContDest->Node('addr', $objAddrShip);
 
    } else {
 
$this->objShip->Node('contact', $objContDest);
 
$this->objCust->Node('contact', $objContCust);
 
$this->objCust->Node('name', $objAddrCard->Name());
 
    }
 
    $this->hasDetails = TRUE;
 
}
 
return $this->objDataTree;
 
 
     }
 
     }
 
 
     /*----
 
     /*----
       ASSUMES: GetDetailObjs() has been called
+
       INPUT: base per-item shipping price
 +
      RETURNS: calculated price for the current shipping zone
 
     */
 
     */
     public function WhoCust() {
+
     public function CalcPerItem($iBase) {
return $this->objCust;
+
return $iBase * $this->PerItemFactor();
 
     }
 
     }
 
     /*----
 
     /*----
       ASSUMES: GetDetailObjs() has been called
+
       INPUT: base per-package shipping price
 +
      RETURNS: calculated price for the current shipping zone
 
     */
 
     */
     public function WhoShip() {
+
     public function CalcPerPkg($iBase) {
return $this->objShip;
+
return $iBase * $this->PerPkgFactor();
    }
 
}
 
class clsShopCartLines extends clsTable {
 
    const TableName='shop_cart_line';
 
 
 
    public function __construct($iDB) {
 
parent::__construct($iDB);
 
  $this->Name(self::TableName);
 
  $this->KeyName('ID');
 
  $this->ClassSng('clsShopCartLine');
 
$this->seqCart = 0;
 
    }
 
    public function Add($iCart, $iCatNum, $iQty) {
 
$objItems = $this->objDB->Items();
 
$objItem = $objItems->Get_byCatNum($iCatNum);
 
if (is_null($objItem)) {
 
// TO DO: log error
 
echo 'ERROR: Could not find item for catalog #'.$iCatNum.'<br>';
 
} else {
 
    $sqlCart = $this->objDB->SafeParam($iCart);
 
    $sqlWhere = '(ID_Cart='.$sqlCart.') AND (ID_Item='.$objItem->ID.')';
 
    $objLine = $this->GetData($sqlWhere,'clsShopCartLine');
 
    $objLine->NextRow(); // load the only data row
 
 
 
    if (!$objLine->hasRows()) {
 
$objLine->ID_Cart=$iCart;
 
    }
 
    $objLine->ID_Item = $objItem->ID;
 
    $objLine->Qty($iQty);
 
    $objLine->Build();
 
}
 
 
     }
 
     }
 
}
 
}
class clsShopCartLine extends clsDataSet {
 
    public function __construct(clsDatabase $iDB=NULL, $iRes=NULL, array $iRow=NULL) {
 
parent::__construct($iDB,$iRes,$iRow);
 
// this is necessary so $this->Update() will work
 
// ...but it should be done by whatever code creates the object
 
//$this->Table = $this->objDB->CartLines();
 
$this->ID = -1;
 
    }
 
    public function IsLoaded() {
 
return ($this->hasRows());
 
    }
 
    public function Cart() {
 
return $this->objDB->Carts()->GetItem($this->ID_Cart);
 
    }
 
    public function Item() {
 
$doLoad = FALSE;
 
if (empty($this->objItem)) {
 
    $doLoad = TRUE;
 
} elseif ($this->objItem->ID != $this->ID_Item) {
 
    $doLoad = TRUE;
 
}
 
if ($doLoad) {
 
    $this->objItem = $this->objDB->Items()->GetItem($this->ID_Item);
 
}
 
return $this->objItem;
 
    }
 
    public function Build() {
 
if ($this->IsLoaded()) {
 
// if ($this->HasRows()) {
 
    $sql = 'UPDATE `'.clsShopCartLines::TableName.'` SET'
 
      .' Qty='.$this->Qty
 
      .' WhenEdited=NOW()'
 
.' WHERE ID='.$this->ID;
 
    $this->objDB->Exec($sql);
 
} else {
 
    $this->Seq = $this->Cart()->LineCount()+1;
 
    $sql = 'INSERT INTO `'.clsShopCartLines::TableName
 
      .'` (Seq,ID_Cart,ID_Item,Qty,WhenAdded)'
 
      .' VALUES('
 
.$this->Seq.', '
 
.$this->ID_Cart.', '
 
.$this->ID_Item.', '
 
.$this->Qty.', NOW());';
 
    $this->objDB->Exec($sql);
 
    $this->ID = $this->objDB->NewID('cartLine.make');
 
}
 
    }
 
    public function Qty($iQty=NULL) {
 
if (!is_null($iQty)) {
 
    if ($this->Qty != $iQty) {
 
$qtyNew = 0+$iQty; // make sure it's an integer -- prevent injection attack
 
$arrSet['Qty'] = SQLValue($qtyNew);
 
$arrSet['WhenEdited'] = 'NOW()';
 
$this->Update($arrSet);
 
$this->Qty = $qtyNew;
 
    }
 
}
 
return $this->Qty;
 
    }
 
/*
 
    protected function ItemSpecs(array $iSpecs=NULL) {
 
if (is_null($iSpecs)) {
 
    $this->Item(); // make sure $this->objItem is loaded
 
    $this->objTitle = $this->objItem->Title();
 
    $this->objItTyp = $this->objItem->ItTyp();
 
    $this->objItOpt = $this->objItem->ItOpt();
 
  
    $out['tname'] = $this->objTitle->Name;
 
    $out['ittyp'] = $this->objItTyp->Name($this->Qty);
 
    $out['itopt'] = $this->objItOpt->Descr;
 
    return $out;
 
} else {
 
    return $iSpecs;
 
}
 
    }
 
    public function ItemDesc(array $iSpecs=NULL) { // plaintext
 
$sp = $this->ItemSpecs($iSpecs);
 
 
$strItOpt = $sp['itopt'];
 
 
$out = '"'.$sp['tname'].'" ('.$sp['ittyp'];
 
if (!is_null($strItOpt)) {
 
    $out .= ' - '.$strItOpt;
 
}
 
$out .= ')';
 
 
return $out;
 
    }
 
    public function ItemDesc_ht(array $iSpecs=NULL) { // as HTML
 
$sp = $this->ItemSpecs($iSpecs);
 
 
$htTitleName = '<i>'.$this->objTitle->LinkName().'</i>';
 
$strItOpt = $sp['itopt'];
 
 
$out = $htTitleName.' ('.$sp['ittyp'];
 
if (!is_null($strItOpt)) {
 
    $out .= ' - '.$strItOpt;
 
}
 
$out .= ')';
 
 
return $out;
 
    }
 
    public function ItemDesc_wt(array $iSpecs=NULL) { // as wikitext
 
$sp = $this->ItemSpecs($iSpecs);
 
 
$wtTitleName = "''".$this->objTitle->LinkName_wt()."''";
 
$strItOpt = $sp['itopt'];
 
 
$out = $wtTitleName.' ('.$sp['ittyp'];
 
if (!is_null($strItOpt)) {
 
    $out .= ' - '.$strItOpt;
 
}
 
$out .= ')';
 
 
return $out;
 
    }
 
*/
 
    /*-----
 
      PURPOSE: Do calculations necessary for rendering the cart line
 
      USED BY:
 
* the shopping cart form
 
* the final order display
 
* the conversion from cart to order
 
    */
 
    public function RenderCalc(clsShipZone $iZone) {
 
$arItem = $this->Item()->DescSpecs();
 
$txtItemDesc = $this->Item()->DescLong($arItem);
 
$htmItemDesc = $this->Item()->DescLong_ht($arItem);
 
 
$objItem = $this->Item();
 
$this->PriceItem = $objItem->PriceSell;
 
$this->Value('CatNum',$objItem->CatNum);
 
 
// save for copying to order line object:
 
$this->DescText = $txtItemDesc;
 
$this->DescHtml = $htmItemDesc;
 
 
// save so cart can figure totals;
 
$idsZone = $iZone->Abbr();
 
$this->ShipPkgDest = $objItem->ShipPricePkg($idsZone);
 
$this->ShipItmDest = $objItem->ShipPriceItem($idsZone);
 
 
// calculate costs:
 
$this->CostItemQty = $this->Qty * $this->PriceItem;
 
$this->CostShipQty = $this->Qty * $this->ShipItmDest;
 
    }
 
    /*
 
      ACTION: Render the current cart line using static HTML (no form elements; read-only)
 
      HISTORY:
 
2011-04-01 adapting this to use clsOrdLine->RenderStatic()
 
    */
 
    public function RenderHtml(clsShopCart $iCart) {
 
 
$objOLine = $this->Engine()->OrdLines()->SpawnItem();
 
$this->RenderCalc($iCart->objShipZone); // calculate some needed fields
 
$objOLine->Init_fromCartLine($this);
 
return $objOLine->RenderStatic($iCart->objShipZone);
 
/*/
 
// calculate display fields:
 
if ($this->Qty) {
 
    $this->RenderCalc($iCart->objShipZone);
 
 
    $strQty = $this->Qty;
 
    $htLineCtrl = $strQty;
 
 
    $mnyPrice = $this->PriceItem; // item price
 
    $mnyPerItm = $this->ShipItmDest; // per-item shipping
 
    $mnyPerPkg = $this->ShipPkgDest; // per-pkg minimum shipping
 
    $mnyPriceQty = $this->CostItemQty; // line total sale
 
    $mnyPerItmQty = $this->CostShipQty; // line total per-item shipping
 
    $mnyLineTotal = $mnyPriceQty + $mnyPerItmQty; // line total overall (does not include per-pkg minimum)
 
 
    $strCatNum = $this->CatNum;
 
    $strPrice = FormatMoney($mnyPrice);
 
    $strPerItm = FormatMoney($mnyPerItm);
 
    $strPriceQty = FormatMoney($mnyPriceQty);
 
    $strPerItmQty = FormatMoney($mnyPerItmQty);
 
    $strLineTotal = FormatMoney($mnyLineTotal);
 
 
    $strShipPkg = FormatMoney($mnyPerPkg);
 
 
    $htDesc = $this->DescHtml;
 
 
    $htDelBtn = '';
 
 
    $out = <<<__END__
 
<tr>
 
<td>$htDelBtn$strCatNum</td>
 
<td>$htDesc</td>
 
<td class=cart-price align=right>$strPrice</td>
 
<td class=shipping align=right>$strPerItm</td>
 
<td class=qty align=right>$htLineCtrl</td>
 
<td class=cart-price align=right>$strPriceQty</td>
 
<td class=shipping align=right>$strPerItmQty</td>
 
<td class=total align=right>$strLineTotal</td>
 
<td class=shipping align=right>$strShipPkg</td>
 
</tr>
 
__END__;
 
    return $out;
 
}
 
/**/
 
    }
 
    /*----
 
      ACTION: Render the current cart line as part of an interactive HTML form
 
    */
 
    public function RenderForm(clsShopCart $iCart) {
 
// calculate display fields:
 
if ($this->Qty) {
 
    $this->RenderCalc($iCart->objShipZone);
 
 
    //$htLineName = 'cart-line-'.$this->Seq;
 
    $htLineName = 'qty-'.$this->CatNum;
 
    $strQty = $this->Qty;
 
    $htLineCtrl = '<input size=2 align=right name="'.$htLineName.'" value='.$strQty.'>';
 
 
    $mnyPrice = $this->PriceItem; // item price
 
    $mnyPerItm = $this->ShipItmDest; // per-item shipping
 
    $mnyPerPkg = $this->ShipPkgDest; // per-pkg minimum shipping
 
    $mnyPriceQty = $this->CostItemQty; // line total sale
 
    $mnyPerItmQty = $this->CostShipQty; // line total per-item shipping
 
    $mnyLineTotal = $mnyPriceQty + $mnyPerItmQty; // line total overall (does not include per-pkg minimum)
 
 
    $strCatNum = $this->CatNum;
 
    $strPrice = FormatMoney($mnyPrice);
 
    $strPerItm = FormatMoney($mnyPerItm);
 
    $strPriceQty = FormatMoney($mnyPriceQty);
 
    $strPerItmQty = FormatMoney($mnyPerItmQty);
 
    $strLineTotal = FormatMoney($mnyLineTotal);
 
 
    $strShipPkg = FormatMoney($mnyPerPkg);
 
 
    $htDesc = $this->DescHtml;
 
 
    $htDelBtn = '<span class=text-btn>[<a href="?item='.$this->ID_Item.'&action=del" title="remove '.$strCatNum.' from cart">remove</a>]</span> ';
 
 
    $out = <<<__END__
 
<tr>
 
<td>$htDelBtn$strCatNum</td>
 
<td>$htDesc</td>
 
<td class=cart-price align=right>$strPrice</td>
 
<td class=shipping align=right>$strPerItm</td>
 
<td class=qty align=right>$htLineCtrl</td>
 
<td class=cart-price align=right>$strPriceQty</td>
 
<td class=shipping align=right>$strPerItmQty</td>
 
<td class=total align=right>$strLineTotal</td>
 
<td class=shipping align=right>$strShipPkg</td>
 
</tr>
 
__END__;
 
    return $out;
 
/**/
 
}
 
    }
 
    public function RenderText(clsShopCart $iCart,$iFmt) {
 
if ($this->Qty) {
 
    $this->RenderCalc($iCart->objShipZone);
 
 
    $dlrPrice = $this->PriceItem; // item price
 
    $dlrPerItm = $this->ShipItmDest; // per-item shipping
 
    $dlrPerPkg = $this->ShipPkgDest; // per-pkg minimum shipping
 
    $dlrPriceQty = $this->CostItemQty; // line total sale
 
    $dlrPerItmQty = $this->CostShipQty; // line total per-item shipping
 
    $dlrLineTotal = $dlrPriceQty + $dlrPerItmQty; // line total overall (does not include per-pkg minimum)
 
 
    $ftCatNum = $this->CatNum;
 
    $ftPrice = FormatMoney($dlrPrice);
 
    $ftPerItm = FormatMoney($dlrPerItm);
 
    $ftQty = $this->Qty;
 
    $ftPriceQty = FormatMoney($dlrPriceQty); // price x qty
 
    $ftPerItmQty = FormatMoney($dlrPerItmQty); // per-item shipping x qty
 
    $ftLineTotal = FormatMoney($dlrLineTotal);
 
 
    $ftShipPkg = FormatMoney($dlrPerPkg);
 
 
    $ftDesc = $this->DescText;
 
 
    $out = "\n".sprintf($iFmt,$ftCatNum,$ftPrice,$ftPerItm,$ftQty,$ftPriceQty,$ftPerItmQty,$ftLineTotal);
 
    $out .= "\n - $ftDesc";
 
    return $out;
 
    }
 
/* (2010-02-18) actually, this is probably superceded by RenderText()
 
    public function RenderEmail($iCart) {
 
/* TO DO: to be written; the following is from Perl:
 
      $out .= "$intLine. $lineDescText\n";
 
      $out .= "Catalog #: $lineCatNum\n";
 
      $out .= "Price each        : ".AlignDollars($linePrice)."\n";
 
      $out .= "Shipping each    : ".AlignDollars($lineShipItemDest)."\n";
 
      $out .= "Min. base shipping: ".AlignDollars($lineShipPkgDest)."\n";
 
      if ($lineQty != 1) {
 
      $textOrder .= "##### QTY $strQty #####\n";
 
      }
 
      $out .= "\n";
 
*/
 
    }
 
}
 
 
// ShopCart Log
 
// ShopCart Log
 
class clsShopCartLog extends clsTable {
 
class clsShopCartLog extends clsTable {
Line 2,743: Line 376:
 
     */
 
     */
 
     public function IsValidNow($iKey) {
 
     public function IsValidNow($iKey) {
  $ok = ($this->Token == $iKey);
+
$ok = ($this->Token == $iKey);
  if ($ok) {
+
if ($ok) {
      $idClientWas = $this->ID_Client;
+
    $idClientWas = $this->ID_Client;
      $objClient = $this->Client();
+
    $objClient = $this->Client();
      if ($idClientWas != $this->ID_Client) {
+
    if ($idClientWas != $this->ID_Client) {
  // not an error, but could indicate a hacking attempt -- so log it, flagged as severe:
+
// not an error, but could indicate a hacking attempt -- so log it, flagged as severe:
  $this->objDB->LogEvent(
+
$this->objDB->LogEvent(
    'session.valid',
+
  'session.valid',
    'KEY='.$iKey,' OLD-CLIENT='.$idClientWas.' NEW-CLIENT='.$this->ID_Client,
+
  'KEY='.$iKey,' OLD-CLIENT='.$idClientWas.' NEW-CLIENT='.$this->ID_Client,
    'stored session client mismatch','XCRED',FALSE,TRUE);
+
  'stored session client mismatch','XCRED',FALSE,TRUE);
  $ok = FALSE;
+
$ok = FALSE;
      }
+
    }
  }
+
}
  return $ok;
+
return $ok;
 
     }
 
     }
 
     public function SetCart($iID) {
 
     public function SetCart($iID) {
Line 2,774: Line 407:
 
     public function SessKey() {
 
     public function SessKey() {
 
return $this->ID.'-'.$this->Token;
 
return $this->ID.'-'.$this->Token;
    }
 
    /*----
 
      RETURNS: TRUE if the cart is usable within the current context
 
If we're displaying checkout stuff, it's okay to use a cart
 
  which has already been turned into an order.
 
If we're still in the cart-editing phase, then we need to
 
  fetch a new cart if the old one has been ordered (or, later,
 
  give the user options -- add to existing order, edit existing order,
 
  create completely new order...).
 
    */
 
    private function IsCartUsable($iCart) {
 
if ($this->Engine()->inCkout) {
 
    // for voided, still need a new cart; ordered is ok
 
    return !$iCart->IsVoided();
 
} else {
 
    // ordered or voided means we need a new cart
 
    return !$iCart->IsLocked();
 
}
 
 
     }
 
     }
 
     /*----
 
     /*----
Line 2,803: Line 418:
 
     public function Cart() { // DEPRECATED FORM
 
     public function Cart() { // DEPRECATED FORM
 
return $this->CartObj();
 
return $this->CartObj();
    }
 
    public function CartObj() {
 
// if there's a cart for this session, load it; otherwise create a new one but don't save it:
 
if (!isset($this->objCart)) {
 
    $objCarts = $this->objDB->Carts();
 
    if (!is_null($this->ID_Cart)) {
 
$objCart = $objCarts->GetItem($this->ID_Cart);
 
/*
 
// KLUGE for testing - should not normally be necessary:
 
if ($this->objCart->ID_Sess==0) {
 
    $this->objCart->ID_Sess = $this->ID;
 
    $this->objCart->Update(array('ID_Sess'=>$this->ID));
 
}
 
*/
 
if (!$this->IsCartUsable($objCart)) {
 
    $this->ID_Cart = NULL; // get a new cart if the order is locked
 
}
 
    }
 
    if (is_null($this->ID_Cart)) {
 
$objCart = $this->objDB->Carts()->SpawnItem();
 
$objCart->InitNew($this->ID);
 
    }
 
}
 
assert('is_object($objCart);'); // we should always have a cart at this point
 
$this->objCart = $objCart;
 
return $objCart;
 
 
     }
 
     }
 
     public function Client() {
 
     public function Client() {
Line 2,900: Line 489:
 
}
 
}
  
/* ======================
 
ORDER MANAGEMENT CLASSES
 
Should probably be in a separate file
 
LATER: Split into 3 parts:
 
  (1) core pieces, like table name
 
  (2) pieces only used by cart
 
  (3) (already done - class VbzAdminOrders) pieces only used by admin system
 
*/
 
class clsOrders extends clsTable {
 
    const TableName='core_orders';
 
  
    public function __construct($iDB) {
 
parent::__construct($iDB);
 
  $this->Name(self::TableName);
 
  $this->KeyName('ID');
 
  $this->ClassSng('clsOrder');
 
  $this->ActionKey(KS_URL_PAGE_ORDER);
 
    }
 
    /*-----
 
    | FUNCTION: Create()
 
    | ACTION: create the order record (fill in minimal fields)
 
    */
 
    public function Create() {
 
$intSeq = $this->NextOrdSeq();
 
$strSeq = sprintf(KS_ORD_NUM_FMT,$intSeq);
 
$strNum = KC_ORD_NUM_PFX.$strSeq;
 
$arIns = array(
 
    'Number' => SQLValue($strNum),
 
    'SortPfx' => SQLValue(KC_ORD_NUM_SORT),
 
    'WhenStarted' => 'NOW()'
 
    );
 
$this->Insert($arIns);
 
$id = $this->objDB->NewID();
 
assert('$id > 0');
 
return $id;
 
    }
 
    /*-----
 
    | FUNCTION: NextOrdSeq()
 
    | ACTION: get the next order sequence number
 
    */
 
    private function NextOrdSeq() {
 
$objVars = $this->objDB->VarsGlobal();
 
$intOrdLast = $objVars->Val('ord_seq_prev');
 
$intOrdThis = $intOrdLast+1;
 
$objVars->Val('ord_seq_prev',$intOrdThis);
 
return $intOrdThis;
 
    }
 
    /*-----
 
    | FUNCTION: Populate()
 
    | ACTION: fill in the order record with data from the cart
 
    */
 
    public function CopyCart($iOrdID, clsShopCart $iCartObj) {
 
assert('$iOrdID > 0');
 
 
// ** ITEMS IN CART (convert from cart lines to order items)
 
//$objTbl = new clsShopCartLines($this->objDB);
 
//$objRows = $objTbl->GetData('ID_Cart='.$this->ID);
 
 
$objOrd = $this->GetItem($iOrdID);
 
$objOrd->CopyCart($iCartObj);
 
 
// should this code be in $objOrd->CopyCart?
 
// in session object, set Order ID and clear Cart ID
 
// 2011-03-27 wrong. just set Order ID.
 
$arUpd = array(
 
  'ID_Order' => $iOrdID,
 
  //'ID_Cart' => 'NULL'
 
  );
 
$iCartObj->Session()->Update($arUpd);
 
// log the event
 
$this->Engine()->LogEvent(
 
  __METHOD__,
 
  '|ord ID='.$iOrdID.'|cart ID='.$iCartObj->KeyValue(),
 
  'Converted cart to order; SQL='.SQLValue($iCartObj->Session()->sqlExec),
 
  'C>O',FALSE,FALSE);
 
 
return $objOrd; // this is used by the checkout process
 
    }
 
}
 
class clsOrder extends clsVbzRecs {
 
    /*----
 
      NOTE: "Code" is now an integer. It used to be a string.
 
      HISTORY:
 
2010-10-08 Moved here from VbzAdminOrder
 
    */
 
/*
 
    public function StartEvent($iWhere,$iCode,$iDescr,$iNotes=NULL) {
 
$arEvent = array(
 
  //'type' => clsEvents::kTypeOrd,
 
  'type' => $this->Table->ActionKey(),
 
  'id' => $this->ID,
 
  'where' => $iWhere,
 
  'code' => $iCode,
 
  'descr' => $iDescr
 
  );
 
if (!is_null($iNotes)) {
 
    $arEvent['notes'] = $iNotes;
 
}
 
$this->idEvent = $this->objDB->Events()->StartEvent($arEvent);
 
    }
 
*/
 
    protected function StartEvent_Simple($iCode,$iDescr,$iWhere) {
 
//$this->objDB->OrderLog()->Add($this->ID,$iCode,$iDescr);
 
$arEv = array(
 
  'descr' => $iDescr,
 
  'type' => $this->Table->ActionKey(),
 
  'id' => $this->KeyValue(),
 
  'where' => $iWhere,
 
  'code' => $iCode);
 
// $this->objDB->Events()->StartEvent($arEv);
 
$this->StartEvent($arEv);
 
    }
 
    /*====
 
      HISTORY:
 
2010-12-05 boilerplate event logging added to VbzAdminSupplier
 
2011-03-23 copied from VbzAdminSupplier to VbzAdminOrder
 
2011-03-27 moved from VbzAdminOrder to clsOrder
 
    */
 
    public 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();
 
    }
 
    //====
 
/*
 
    protected function FinishEvent(array $iArgs=NULL) {
 
$this->objDB->Events()->FinishEvent($iArgs);
 
    }
 
*/
 
    /*----
 
      HISTORY:
 
2010-10-08 Moved here from VbzAdminOrder; supercedes existing protected function
 
  which might not have been working properly anyway (no idEvent)
 
    */
 
/* 2011-03-27 this is now redundant
 
    public function FinishEvent(array $iArgs=NULL) {
 
$this->objDB->Events()->FinishEvent($this->idEvent,$iArgs);
 
    }
 
*/
 
    /*-----
 
      TO DO: Explain why this object sometimes doesn't know what the cart is yet
 
(presumably happens during the cart->order transfer process)
 
      USED BY:
 
Created for VbzAdmin, where it is sometimes necessary to pass the cart ID (why?)
 
Now used by this class for displaying order receipt.
 
    */
 
    protected function Cart($iID=NULL) {
 
if (is_null($iID)) {
 
    $idCart = $this->ID_Cart;
 
} else {
 
    $idCart = $iID;
 
}
 
 
$doLoad = TRUE;
 
if (isset($this->objCart)) {
 
    if ($this->objCart->ID == $idCart) {
 
$doLoad = FALSE;
 
    }
 
}
 
if ($doLoad) {
 
    $objCart = $this->objDB->Carts()->GetItem($idCart);
 
    $this->objDB->Cart($objCart);
 
    $this->objCart = $objCart;
 
}
 
return $objCart;
 
    }
 
 
    public function Lines() {
 
$objTbl = $this->objDB->OrdLines();
 
$objRows = $objTbl->GetData('ID_Order='.$this->ID);
 
return $objRows;
 
    }
 
    public function CopyCart(clsShopCart $iCartObj) {
 
$this->CopyCartData($iCartObj);
 
$this->CopyCartLines($iCartObj);
 
    }
 
    /* =====
 
      ACTION: Copy over basic cart information (totals, etc.)
 
      HISTORY:
 
2010-10-06 added cart ID to update -- otherwise final order confirmation page can't find cart data
 
    */
 
    private function CopyCartData(clsShopCart $iCartObj) {
 
$curItemTotal = $iCartObj->DataItem(KSI_ITEM_TOTAL);
 
$curShipItem = $iCartObj->DataItem(KSI_PER_ITEM_TOTAL);
 
$curShipPkg = $iCartObj->DataItem(KSI_PER_PKG_TOTAL);
 
$idCart = $iCartObj->ID;
 
$this->ID_Cart;
 
 
$arUpd = array(
 
  'ID_Cart' => $idCart,
 
  'WebTotal_Merch' => SQLValue($curItemTotal),
 
  'WebTotal_Ship' => SQLValue($curShipItem+$curShipPkg),
 
  'WebTotal_Final' => SQLValue($curItemTotal+$curShipItem+$curShipPkg)
 
  );
 
$this->StartEvent_Simple('CDC','Copying data from cart ID='.$idCart,__METHOD__);
 
$this->Update($arUpd); // we're assuming the order record exists at this point
 
$this->FinishEvent();
 
    }
 
    /*-----
 
    ACTION: Create order lines from cart lines
 
    */
 
    private function CopyCartLines(clsShopCart $iCartObj) {
 
$objRows = $iCartObj->GetLines(); // shopping cart lines to convert
 
$objTbl = new clsOrderLines($this->objDB);
 
$objTbl->Update(array('QtyOrd'=>0),'ID_Order='.$this->ID); // zero out any existing order lines in case customer edits cart
 
if ($objRows->HasRows()) {
 
    $this->StartEvent_Simple('CLC','copying cart lines',__METHOD__);
 
    $intNew = 0;
 
    $intUpd = 0;
 
    while ($objRows->NextRow()) {
 
$intSeq = $objRows->Seq;
 
$idItem = $objRows->ID_Item;
 
$intQty = $objRows->Qty;
 
$dtWhenAdded = $objRows->WhenAdded;
 
$dtWhenEdited = $objRows->WhenEdited;
 
 
$objRows->RenderCalc($iCartObj->objShipZone);
 
 
$arUpd = array(
 
  'CatNum' => SQLValue($objRows->CatNum),
 
  'Descr' => SQLValue($objRows->Item()->DescLong()),
 
  'QtyOrd' => $intQty,
 
  'Price' => SQLValue($objRows->PriceItem),
 
  'ShipPkg' => SQLValue($objRows->ShipPkgDest),
 
  'ShipItm' => SQLValue($objRows->ShipItmDest),
 
  'isShipEst' => 'FALSE'
 
  );
 
 
// has this item already been transcribed?
 
$sqlFilt = '(ID_Order='.$this->ID.') AND (ID_Item='.$idItem.')';
 
$objOrdItems = $objTbl->GetData($sqlFilt);
 
if ($objOrdItems->RowCount() > 0)  {
 
    // already transcribed -- update existing record
 
    $objTbl->Update($arUpd,$sqlFilt);
 
    $intUpd++;
 
} else {
 
    // not already transcribed -- insert new record
 
    $arIns = $arUpd + array(
 
      'ID_Order' => $this->ID,
 
      'Seq' => $intSeq,
 
      'ID_Item' => $idItem
 
      );
 
    $objTbl->Insert($arIns);
 
    $intNew++;
 
}
 
    }
 
    $strLines = $intNew.' order line'.Pluralize($intNew).' created';
 
    if ($intUpd > 0) {
 
$strLines .= ' and '.$intUpd.' order line'.Pluralize($intUpd).' updated';
 
    }
 
    $strLines .= ' from cart lines';
 
    $this->FinishEvent(array('descr'=>$strLines));
 
} else {
 
    $this->StartEvent_Simple('CL0','No cart lines found at order creation time',__METHOD__);
 
    $out = '<b>There has been an error</b>: your cart contents seem to be missing.';
 
    // TO DO: send email alerting webmaster; print more helpful message.
 
}
 
    }
 
    /*----
 
      PURPOSE: nomenclatural purity. I could have just added the display code to RenderReceipt(),
 
but it seemed like a good idea to have a function that just generates the output without
 
displaying it. It also seemed like a good idea not to add the code to the calling function,
 
because that's untidy, So here we are, with a function shorter than its comments.
 
      NOTE: admin functions will generally want to handle the display themselves, so it is good practice
 
to be able to retrieve the output to be displayed without displaying it.
 
It may be redundant to have ShowReceipt() at all, however.
 
    */
 
    public function ShowReceipt() {
 
$out = $this->RenderReceipt();
 
echo $out;
 
    }
 
    /*----
 
      ACTION: Render the receipt in HTML
 
Shows the order as generated from *cart* data, not what's in the order record.
 
...except for the order number.
 
    */
 
    public function RenderReceipt() {
 
$out = NULL;
 
 
// 2010-10-17 This should have been loaded earlier, but I don't have time to track it down now.
 
// The problem is that it forgets the cart ID otherwise.
 
$this->Reload();
 
 
// get core objects, for code clarity/portability
 
$objOrd = $this;
 
$objCart = $this->Cart();
 
$idOrder = $this->KeyValue();
 
$idCart = $this->Value('ID_Cart');
 
 
if (!is_object($objCart)) {
 
    throw new exception("Receipt could not get cart object: Cart ID=[$idCart]");
 
}
 
if (($objOrd->IsNew()) || ($objCart->IsNew())) {
 
    throw new exception("Receipt has missing object: Order ID=[$idOrder] Cart ID=[$idCart]");
 
}
 
 
$objCart->GetDetailObjs();
 
$objPay = $objCart->PersonCustObj()->Payment();
 
$objAddrCard = $objCart->AddrCardObj();
 
// the next line is a kluge which only works as long as payment is always ccard
 
// it's also not clear why GetDetailObjs() isn't loading it properly
 
$objPay->Node('addr', $objAddrCard);
 
 
$arVars = array(
 
  'ord.num' => $objOrd->Number,
 
  'timestamp' => date(KF_RCPT_TIMESTAMP),
 
  'cart.id' => $objCart->ID,
 
  'cart.detail' => $objCart->RenderConfirm(),
 
  'ship.name' => $objCart->AddrShipObj()->Name()->Value(),
 
  'ship.addr' => $objCart->AddrShipObj()->AsText("\n<br>"),
 
  'pay.name' => $objPay->Addr()->Name()->Value(),
 
  'pay.spec' => $objPay->SafeDisplay(),
 
  'email.short' => 'orders-'.date('Y').'@vbz.net'
 
  );
 
$objStrTplt = new clsStringTemplate_array(NULL,NULL,$arVars);
 
$objStrTplt->MarkedValue(KHT_RCPT_TPLT);
 
$out = "<!-- ORDER ID: [$idOrder] / CART ID from order: [$idCart] -->";
 
$out .= $objStrTplt->Replace();
 
return $out;
 
 
// return $objCart->RenderReceipt();
 
    }
 
    /*----
 
      RETURNS: Email address to use for this order (includes order number in address, as a way of tracking
 
spam and possibly later as a way of automatically filing order-related correspondence)
 
    */
 
    public function EmailAddr() {
 
return 'order-'.$this->Number.'@vbz.net';
 
    }
 
    /*----
 
      RETURNS: Order receipt (from cart data) in text-only format, suitable for emailing
 
      HISTORY:
 
2010-09-13 writing started
 
    */
 
    public function RenderReceipt_Text() {
 
$out = $this->Cart()->RenderOrder_Text();
 
return $out;
 
    }
 
    /*----
 
      RETURNS: array of values needed to plug into email templates
 
      USED BY:
 
cart class, to generate confirmation email
 
order admin class, to display/generate confirmation email
 
    */
 
    public function TemplateVars() {
 
// global $ksTextEmail;
 
$this->Reload(); // 2010-10-28 kluge attempt - see also RenderReceipt()
 
 
$objCart = $this->Cart();
 
$objCust = $objCart->PersonCustObj();
 
$objPay = $objCust->Payment();
 
 
assert('is_object($objCart)');
 
assert('is_object($objCart)');
 
assert('is_object($objPay)');
 
 
$objCardAddr = $objCart->AddrCardObj();
 
$objCustCont = $objCart->ContCustObj();
 
 
assert('is_object($objCardAddr)');
 
assert('is_object($objCustCont)');
 
 
$arVars = array(
 
// THIS IS A KLUGE -- should be something like objCart->CustObj->Name
 
  'cust-name' => $objCardAddr->Name()->Value(),
 
  'cust-email' => $objCustCont->Email()->Value(),
 
  'orders-email' => $this->EmailAddr(),
 
  'ord-num' => $this->Number
 
  );
 
return $arVars;
 
    }
 
    /*----
 
      RETURNS: array of values needed for sending confirmation email
 
      USED BY:
 
cart class, to generate confirmation email
 
order admin class, to display/generate confirmation email
 
    */
 
    public function EmailParams(array $iarVars, $iSubj=NULL, $iMsgPre=NULL) {
 
$arVars = $iarVars;
 
$objStrTplt = new clsStringTemplate_array(KS_TPLT_OPEN,KS_TPLT_SHUT,$arVars);
 
 
$txtSubj = $iSubj;
 
if (empty($txtSubj)) {
 
    //$objStrTplt->MarkedValue(KS_TEXT_EMAIL_SUBJECT);
 
    $txtSubj = $objStrTplt->Replace(KS_TPLT_EMAIL_SUBJECT);
 
}
 
$arOut['subj'] = $txtSubj;
 
 
$txtMsgPre = $iMsgPre;
 
if (empty($txtMsgPre)) {
 
    $txtMsgPre = $objStrTplt->Replace(KS_TPLT_EMAIL_MSG_TOP);
 
}
 
$arOut['msg.pre'] = $txtMsgPre;
 
 
$arVars['subject'] = $txtSubj;
 
$objStrTplt->List = $arVars;
 
 
//$objStrTplt->MarkedValue(KS_TEXT_EMAIL_ADDR_SELF);
 
$txtAddr_Self = $objStrTplt->Replace(KS_TPLT_EMAIL_ADDR_SELF);
 
$arOut['addr.self'] = $txtAddr_Self;
 
 
//$objStrTplt->MarkedValue(KS_TEXT_EMAIL_ADDR_CUST);
 
$txtAddr_Cust = $objStrTplt->Replace(KS_TPLT_EMAIL_ADDR_CUST);
 
$arOut['addr.cust'] = $txtAddr_Cust;
 
 
// Calculate text of email to send:
 
$txtEmailBody = $txtMsgPre."\n".$this->RenderReceipt_Text();
 
$arOut['msg.body'] = $txtEmailBody;
 
 
return $arOut;
 
    }
 
    /*----
 
      ACTION: Email an order confirmation (or show what would be sent)
 
      RETURNS: contents of the email
 
      HISTORY:
 
2010-10-08 moved from VbzAdminOrder to clsOrder
 
      INPUT:
 
iReally: if FALSE, does not actually send the email, but only renders (and returns) it
 
iParams: array of specifications for how to send the email
 
  iParams['to-self']: if TRUE, send a copy to the store admin's address
 
  iParams['to-cust']: if TRUE, send a copy to the customer's address
 
  iParams['addr-self']: email address for store admin
 
  iParams['addr-cust']: email address for customer
 
  iParams['subject']: subject of the email
 
  iParams['message']: body of the email
 
    */
 
    //public function EmailConfirm($iReally, $iSendToSelf, $iSendToCust, $iAddrSelf, $iAddrCust, $iSubject, $iMessage) {
 
    public function EmailConfirm($iReally, array $iParams, clsLogger $iLog) {
 
$iSendToSelf = $iParams['to-self'];
 
$iSendToCust = $iParams['to-cust'];
 
$iAddrSelf = $iParams['addr-self'];
 
$iAddrCust = $iParams['addr-cust'];
 
$iSubject = $iParams['subject'];
 
$iMessage = $iParams['message'];
 
$htEmailAddr_Self = htmlspecialchars($iAddrSelf);
 
$htEmailAddr_Cust = htmlspecialchars($iAddrCust);
 
 
$htEmailBody = htmlspecialchars($iMessage);
 
$txtSubj = $iSubject;
 
$htSubj = htmlspecialchars($txtSubj);
 
$intCustCopy = $iSendToCust?'1':'0';
 
$txtCustCopy = $iSendToCust?'YES':'no';
 
$txtSelfCopy = $iSendToSelf?'YES':'no';
 
$doSend = $iReally;
 
 
$out = NULL;
 
$out .= '<table>';
 
$out .= "<tr><td align=right><b>Store's email address</b>:</td><td>$htEmailAddr_Self</td></tr>";
 
$out .= "<tr><td align=right><b>Customer's email address</b>:</td><td>$htEmailAddr_Cust</td></tr>";
 
$out .= "<tr><td align=right><b>Subject</b>:</td><td>$htSubj</td></tr>";
 
$out .= "<tr><td align=right><b>Copying to customer?</td><td>$txtCustCopy</td></tr>";
 
$out .= '</table>';
 
     
 
$out .= '<pre>'.$htEmailBody.'</pre>';
 
if (!$doSend) {
 
    $out .= '<form method=post>';
 
    $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 .= '</form>';
 
}
 
 
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__
 
<tr>
 
<td>$htDelBtn$strCatNum</td>
 
<td>$htDesc</td>
 
<td class=cart-price align=right>$strPrice</td>
 
<td class=shipping align=right>$strPerItm</td>
 
<td class=qty align=right>$htLineCtrl</td>
 
<td class=cart-price align=right>$strPriceQty</td>
 
<td class=shipping align=right>$strPerItmQty</td>
 
<td class=total align=right>$strLineTotal</td>
 
<td class=shipping align=right>$strShipPkg</td>
 
</tr>
 
__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';
 
}
 
    }
 
}
 
  
 
/* ===============
 
/* ===============

Revision as of 02:25, 21 February 2013

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>