CORE-POS/IS4C

View on GitHub
pos/is4c-nf/plugins/Paycards/MercuryGift.php

Summary

Maintainability
F
1 wk
Test Coverage
<?php
/*******************************************************************************

    Copyright 2007,2010 Whole Foods Co-op

    This file is part of IT CORE.

    IT CORE is free software; you can redistribute it and/or modify
    it under the terms of the GNU General Public License as published by
    the Free Software Foundation; either version 2 of the License, or
    (at your option) any later version.

    IT CORE is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    GNU General Public License for more details.

    You should have received a copy of the GNU General Public License
    in the file license.txt along with IT CORE; if not, write to the Free Software
    Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA

*********************************************************************************/

use COREPOS\pos\lib\Database;
use COREPOS\pos\lib\TransRecord;
use COREPOS\pos\plugins\Paycards\xml\BetterXmlData;

/*
 * Mercury Gift Card processing module
 *
 */

if (!class_exists("AutoLoader")) include_once(realpath(dirname(__FILE__).'/../../lib/AutoLoader.php'));
if (!class_exists("PaycardLib")) include_once(realpath(dirname(__FILE__)."/lib/PaycardLib.php"));

define('MERCURY_GTERMINAL_ID',"");
define('MERCURY_GPASSWORD',"");

class MercuryGift extends BasicCCModule 
{
    protected $SOAPACTION = "http://www.mercurypay.com/GiftTransaction";
    private $secondTry;
    // BEGIN INTERFACE METHODS

    private $pmod;
    private $dialogs;
    public function __construct()
    {
        $this->pmod = new PaycardModule();
        $this->dialogs = new PaycardDialogs();
        $this->pmod->setDialogs($this->dialogs);
        $this->conf = new PaycardConf();
    }

    /* handlesType($type)
     * $type is a constant as defined in paycardLib.php.
     * If you class can handle the given type, return
     * True
     */
    public function handlesType($type)
    {
        if ($type == PaycardLib::PAYCARD_TYPE_GIFT) {
            return true;
        } elseif ($type == PaycardLib::PAYCARD_TYPE_ENCRYPTED_GIFT) {
            return true;
        }
        return false;
    }

    /* entered($validate)
     * This function is called in paycardEntered()
     * [paycardEntered.php]. This function exists
     * to move all type-specific handling code out
     * of the paycard* files
     */
    public function entered($validate,$json)
    {
        try {
            $this->dialogs->enabledCheck();

            // error checks based on processing mode
            if ($this->conf->get("paycard_mode") == PaycardLib::PAYCARD_MODE_VOID) {
                $pan4 = substr($this->getPAN(), -4);
                $trans = array($this->conf->get('CashierNo'), $this->conf->get('laneno'), $this->conf->get('transno'));
                $result = $this->dialogs->voidableCheck($pan4, $trans);
                return $this->paycardVoid($result,-1,-1,$json);
            }

            // check card data for anything else
            if ($validate) {
                $this->dialogs->validateCard($this->conf->get('paycard_PAN'), false, false);
            }

            if ($this->conf->get("paycard_mode") == PaycardLib::PAYCARD_MODE_AUTH) {
                $dbc = Database::tDataConnect();
                $res = $dbc->query("SELECT SUM(total) AS ttl FROM localtemptrans WHERE department=902");
                $row = $dbc->fetchRow($res);
                if ($row && abs($row['ttl']) > 0.005) {
                    throw new Exception(PaycardLib::paycardErrBox("Gift Card Error",
                                                     "cannot pay for gift card with a gift card",
                                                     "[clear] to cancel"
                                                 ));
                }
            }

        } catch (Exception $ex) {
            $json['output'] = $ex->getMessage();
            return $json;
        }

        // other modes
        switch ($this->conf->get("paycard_mode")) {
            case PaycardLib::PAYCARD_MODE_AUTH:
                $this->conf->set('CacheCardType', 'GIFT');
                return PaycardLib::setupAuthJson($json);
            case PaycardLib::PAYCARD_MODE_ACTIVATE:
            case PaycardLib::PAYCARD_MODE_ADDVALUE:
                $this->conf->set("paycard_amount",0);
                $this->conf->set("paycard_id",$this->conf->get("LastID")+1); // kind of a hack to anticipate it this way..
                $pluginInfo = new Paycards();
                $json['main_frame'] = $pluginInfo->pluginUrl().'/gui/paycardboxMsgGift.php';

                return $json;
            case PaycardLib::PAYCARD_MODE_BALANCE:
                $pluginInfo = new Paycards();
                $json['main_frame'] = $pluginInfo->pluginUrl().'/gui/paycardboxMsgBalance.php';

                return $json;
        } // switch mode
    
        // if we're still here, it's an error
        $json['output'] = $this->dialogs->invalidMode();
        $this->conf->reset();

        return $json;
    }

    /* doSend()
     * Process the paycard request and return
     * an error value as defined in paycardLib.php.
     *
     * On success, return PaycardLib::PAYCARD_ERR_OK.
     * On failure, return anything else and set any
     * error messages to be displayed in
     * $this->conf->["boxMsg"].
     */
    public function doSend($type)
    {
        $this->secondTry = false;
        switch($type) {
            case PaycardLib::PAYCARD_MODE_ACTIVATE:
            case PaycardLib::PAYCARD_MODE_ADDVALUE:
            case PaycardLib::PAYCARD_MODE_AUTH: 
                return $this->sendAuth();
            case PaycardLib::PAYCARD_MODE_VOID:
                return $this->sendVoid();
            case PaycardLib::PAYCARD_MODE_BALANCE:
                return $this->sendBalance();
            default:
                return $this->setErrorMsg(0);
        }
    }

    /* cleanup()
     * This function is called when doSend() returns
     * PaycardLib::PAYCARD_ERR_OK. (see paycardAuthorize.php)
     * I use it for tendering, printing
     * receipts, etc, but it's really only for code
     * cleanliness. You could leave this as is and
     * do all the everything inside doSend()
     */
    public function cleanup($json)
    {
        switch ($this->conf->get("paycard_mode")) {
            case PaycardLib::PAYCARD_MODE_BALANCE:
                $resp = $this->conf->get("paycard_response");
                $this->conf->set("boxMsg","<b>Success</b><font size=-1>
                                           <p>Gift card balance: $" . $resp["Balance"] . "
                                           <p>\"rp\" to print
                                           <br>[enter] to continue</font>"
                );
                break;
            case PaycardLib::PAYCARD_MODE_ADDVALUE:
            case PaycardLib::PAYCARD_MODE_ACTIVATE:
                $ttl = $this->conf->get("paycard_amount");
                $dept = $this->conf->get('PaycardDepartmentGift');
                $dept = $dept == '' ? 902 : $dept;
                $deptObj = new COREPOS\pos\lib\DeptLib($this->conf);
                $deptObj->deptkey($ttl*100, $dept . '0');
                $resp = $this->conf->get("paycard_response");    
                $this->conf->set("boxMsg","<b>Success</b><font size=-1>
                                           <p>New card balance: $" . $resp["Balance"] . "
                                           <p>[enter] to continue
                                           <br>\"rp\" to reprint slip</font>"
                );
                break;
            case PaycardLib::PAYCARD_MODE_AUTH:
                $amt = "".(-1*($this->conf->get("paycard_amount")));
                $recordID = $this->last_paycard_transaction_id;
                $charflag = ($recordID != 0) ? 'PT' : '';
                $tcode = $this->conf->get('PaycardsTenderCodeGift');
                $tcode = $tcode == '' ? 'GD' : $tcode;
                TransRecord::addFlaggedTender("Gift Card", $tcode, $amt, $recordID, $charflag);
                $resp = $this->conf->get("paycard_response");
                $this->conf->set("boxMsg","<b>Approved</b><font size=-1>
                                           <p>Used: $" . $this->conf->get("paycard_amount") . "
                                           <br />New balance: $" . $resp["Balance"] . "
                                           <p>[enter] to continue
                                           <br>\"rp\" to reprint slip
                                           <br>[clear] to cancel and void</font>"
                );
                break;
            case PaycardLib::PAYCARD_MODE_VOID:
                $void = new COREPOS\pos\parser\parse\VoidCmd($this->conf);
                $void->voidid($this->conf->get("paycard_id"), array());
                $resp = $this->conf->get("paycard_response");
                $this->conf->set("boxMsg","<b>Voided</b><font size=-1>
                                           <p>New balance: $" . $resp["Balance"] . "
                                           <p>[enter] to continue
                                           <br>\"rp\" to reprint slip</font>"
                );
                break;
        }

        return $json;
    }

    /* paycardVoid($transID)
     * Argument is trans_id to be voided
     * Again, this is for removing type-specific
     * code from paycard*.php files.
     */
    public function paycardVoid($transID,$laneNo=-1,$transNo=-1,$json=array()) 
    {
        $this->voidTrans = "";
        $this->voidRef = "";
        $ret = $this->pmod->ccVoid($transID, $laneNo, $transNo, $json);

        // save the details
        $this->conf->set("paycard_type",PaycardLib::PAYCARD_TYPE_GIFT);
        $this->conf->set("paycard_mode",PaycardLib::PAYCARD_MODE_VOID);
    
        return $ret;
    }

    // END INTERFACE METHODS
    
    private function sendAuth($domain="w1.mercurypay.com")
    {
        // initialize
        $dbTrans = Database::tDataConnect();
        if (!$dbTrans) {
            return $this->setErrorMsg(PaycardLib::PAYCARD_ERR_NOSEND); // internal error, nothing sent (ok to retry)
        }

        // prepare data for the request
        $today = date('Ymd'); // numeric date only, it goes in an 'int' field as part of the primary key
        $now = date('Y-m-d H:i:s'); // full timestamp
        $cashierNo = $this->conf->get("CashierNo");
        $laneNo = $this->conf->get("laneno");
        $transNo = $this->conf->get("transno");
        $transID = $this->conf->get("paycard_id");
        $amount = $this->conf->get("paycard_amount");
        $amountText = number_format(abs($amount), 2, '.', '');
        switch ($this->conf->get("paycard_mode")) {
            case PaycardLib::PAYCARD_MODE_AUTH:
                $authMethod = $amount < 0 ? 'Return' : 'NoNSFSale';  
                break;
            case PaycardLib::PAYCARD_MODE_ADDVALUE:
                $authMethod = 'Reload';
                break;
            case PaycardLib::PAYCARD_MODE_ACTIVATE:
                $authMethod = 'Issue';
                break;
            default:
                return $this->setErrorMsg(PaycardLib::PAYCARD_ERR_NOSEND);
        }
        $loggedMode = $authMethod === 'NoNSFSale' ? 'Sale' : $authMethod;
        $termID = $this->getTermID();
        $password = $this->getPw();
        $live = 0;
        $manual = ($this->conf->get("paycard_manual") ? 1 : 0);
        $cardPAN = $this->getPAN();
        if (substr($cardPAN, 0, 8) == "02AA0080") {
            $encBlock = new COREPOS\pos\plugins\Paycards\card\EncBlock();
            $e2e = $encBlock->parseEncBlock($cardPAN);
            $cardPAN = str_repeat('*', 12) . $e2e['Last4'];
        }
        $identifier = $this->refnum($transID); 
        
        /**
          Log transaction in newer table
        */
        $insQ = sprintf("INSERT INTO PaycardTransactions (
                    dateID, empNo, registerNo, transNo, transID,
                    processor, refNum, live, cardType, transType,
                    amount, PAN, issuer, name, manual, requestDateTime)
                 VALUES (
                    %d,     %d,    %d,         %d,      %d,
                    '%s',     '%s',    %d,   '%s',     '%s',
                    %.2f,  '%s', '%s',  '%s',  %d,     '%s')",
                    $today, $cashierNo, $laneNo, $transNo, $transID,
                    'MercuryGift', $identifier, $live, 'Gift', $loggedMode,
                    $amountText, $cardPAN,
                    'Mercury', 'Cardholder', $manual, $now);
        $insR = $dbTrans->query($insQ);
        if ($insR === false) {
            return $this->setErrorMsg(PaycardLib::PAYCARD_ERR_NOSEND); // internal error, nothing sent (ok to retry)
        }
        $this->last_paycard_transaction_id = $dbTrans->insertID();

        $msgXml = "<?xml version=\"1.0\"".'?'.">
            <TStream>
            <Transaction>
            <IpPort>9100</IpPort>
            <MerchantID>$termID</MerchantID>
            <TranType>PrePaid</TranType>
            <TranCode>$authMethod</TranCode>
            <InvoiceNo>$identifier</InvoiceNo>
            <RefNo>$identifier</RefNo>
            <Memo>CORE POS 1.0.0</Memo>
            <Account>";
        $msgXml .= $this->getAccount();
        $msgXml .= "</Account>
            <Amount>
            <Purchase>$amountText</Purchase>
            </Amount>
            </Transaction>
            </TStream>";
        

        $soaptext = $this->soapify("GiftTransaction",
            array("tran"=>$msgXml,"pw"=>$password),
            "http://www.mercurypay.com");

        $this->GATEWAY = "https://$domain/ws/ws.asmx";
        if ($this->conf->get("training") == 1) {
            $this->GATEWAY = "https://w1.mercurycert.net/ws/ws.asmx?WSDL";
        }

        return $this->curlSend($soaptext,'SOAP');
    }

    private function sendVoid($domain="w1.mercurypay.com")
    {
        // initialize
        $dbTrans = Database::tDataConnect();
        if (!$dbTrans) {
            return $this->setErrorMsg(PaycardLib::PAYCARD_ERR_NOSEND); // database error, nothing sent (ok to retry)
        }

        // prepare data for the void request
        $today = date('Ymd'); // numeric date only, it goes in an 'int' field as part of the primary key
        $cashierNo = $this->conf->get("CashierNo");
        $laneNo = $this->conf->get("laneno");
        $transNo = $this->conf->get("transno");
        $transID = $this->conf->get("paycard_id");
        $amount = $this->conf->get("paycard_amount");
        $amountText = number_format(abs($amount), 2, '.', '');
        $cardPAN = $this->getPAN();
        $cardTr2 = $this->getTrack2();
        $identifier = date('mdHis'); // the void itself needs a unique identifier, so just use a timestamp minus the year (10 digits only)
        $termID = $this->getTermID();
        $password = $this->getPw();

        // look up the auth code from the original response 
        // (card number and amount should already be in session vars)
        $sql = "SELECT 
                    xApprovalNumber AS xAuthorizationCode 
                FROM PaycardTransactions 
                WHERE dateID='" . $today . "'
                    AND empNo=" . $cashierNo . "
                    AND registerNo=" . $laneNo . "
                    AND transNo=" . $transNo . "
                    AND transID=" . $transID;
        $search = $dbTrans->query($sql);
        if (!$search || $dbTrans->numRows($search) == 0) {
            return PaycardLib::PAYCARD_ERR_NOSEND; // database error, nothing sent (ok to retry)
        }
        $log = $dbTrans->fetchRow($search);
        $authcode = $log['xAuthorizationCode'];
        
        // look up original transaction type
        $sql = "SELECT transType AS mode 
                FROM PaycardTransactions 
                WHERE dateID='" . $today . "'
                    AND empNo=" . $cashierNo . "
                    AND registerNo=" . $laneNo . "
                    AND transNo=" . $transNo . "
                    AND transID=" . $transID;
        $search = $dbTrans->query($sql);
        if (!$search || $dbTrans->numRows($search) == 0) {
            return PaycardLib::PAYCARD_ERR_NOSEND; // database error, nothing sent (ok to retry)
        }
        $row = $dbTrans->fetchRow($search);
        $vdMethod = "";
        switch ($row['mode']) {
            case 'tender':
            case 'Sale':
                $vdMethod='VoidSale';
                break;
            case 'refund':
            case 'Return':
                $vdMethod='VoidReturn';
                break;
            case 'addvalue':
            case 'Reload':
                $vdMethod='VoidReload';
                break;
            case 'activate':
            case 'Issue':
                $vdMethod='VoidIssue';
                break;
        }

        /**
          populate a void record in PaycardTransactions
        */
        $initQ = "INSERT INTO PaycardTransactions (
                    dateID, empNo, registerNo, transNo, transID,
                    previousPaycardTransactionID, processor, refNum,
                    live, cardType, transType, amount, PAN, issuer,
                    name, manual, requestDateTime)
                  SELECT dateID, empNo, registerNo, transNo, transID,
                    paycardTransactionID, processor, refNum,
                    live, cardType, 'VOID', amount, PAN, issuer,
                    name, manual, " . $dbTrans->now() . "
                  FROM PaycardTransactions
                  WHERE
                    dateID=" . $today . "
                    AND empNo=" . $cashierNo . "
                    AND registerNo=" . $laneNo . "
                    AND transNo=" . $transNo . "
                    AND transID=" . $transID;
        $initR = $dbTrans->query($initQ);
        if ($initR === false) {
            return PaycardLib::PAYCARD_ERR_NOSEND; // database error, nothing sent (ok to retry)
        }
        $this->last_paycard_transaction_id = $dbTrans->insertID();

        $msgXml = "<?xml version=\"1.0\"".'?'.">
            <TStream>
            <Transaction>
            <IpPort>9100</IpPort>
            <MerchantID>$termID</MerchantID>
            <TranType>PrePaid</TranType>
            <TranCode>$vdMethod</TranCode>
            <InvoiceNo>$identifier</InvoiceNo>
            <RefNo>$authcode</RefNo>
            <Memo>CORE POS 1.0.0</Memo>
            <Account>";
        $msgXml .= $this->getAccount();
        $msgXml .= "</Account>
            <Amount>
            <Purchase>$amountText</Purchase>
            </Amount>
            </Transaction>
            </TStream>";

        $soaptext = $this->soapify("GiftTransaction",
            array("tran"=>$msgXml,"pw"=>$password),
            "http://www.mercurypay.com");

        $this->GATEWAY = "https://$domain/ws/ws.asmx";
        if ($this->conf->get("training") == 1) {
            $this->GATEWAY = "https://w1.mercurycert.net/ws/ws.asmx?WSDL";
        }

        return $this->curlSend($soaptext,'SOAP');
    }

    private function sendBalance($domain="w1.mercurypay.com")
    {
        // prepare data for the request
        $identifier = date('mdHis'); // the balance check itself needs a unique identifier, so just use a timestamp minus the year (10 digits only)
        $termID = $this->getTermID();
        $password = $this->getPw();

        $msgXml = "<?xml version=\"1.0\"?>
            <TStream>
            <Transaction>
            <IpPort>9100</IpPort>
            <MerchantID>$termID</MerchantID>
            <TranType>PrePaid</TranType>
            <TranCode>Balance</TranCode>
            <InvoiceNo>$identifier</InvoiceNo>
            <RefNo>$identifier</RefNo>
            <Memo>CORE POS</Memo>
            <Account>";
        $msgXml .= $this->getAccount();
        $msgXml .= "</Account>
            </Transaction>
            </TStream>";

        $soaptext = $this->soapify("GiftTransaction",
            array("tran"=>$msgXml,"pw"=>$password),
            "http://www.mercurypay.com");

        $this->GATEWAY = "https://$domain/ws/ws.asmx";
        if ($this->conf->get("training") == 1) {
            $this->GATEWAY = "https://w1.mercurycert.net/ws/ws.asmx?WSDL";
        }

        return $this->curlSend($soaptext,'SOAP');
    }

    public function handleResponse($authResult)
    {
        switch($this->conf->get("paycard_mode")) {
            case PaycardLib::PAYCARD_MODE_AUTH:
            case PaycardLib::PAYCARD_MODE_ACTIVATE:
            case PaycardLib::PAYCARD_MODE_ADDVALUE:
                return $this->handleResponseAuth($authResult);
            case PaycardLib::PAYCARD_MODE_VOID:
                return $this->handleResponseVoid($authResult);
            case PaycardLib::PAYCARD_MODE_BALANCE:
                return $this->handleResponseBalance($authResult);
        }
    }

    private function handleResponseAuth($authResult)
    {
        $resp = $this->desoapify("GiftTransactionResult",
            $authResult["response"]);
        $xml = new BetterXmlData($resp);

        // initialize
        $dbTrans = Database::tDataConnect();

        // prepare data for the request
        $now = date('Y-m-d H:i:s'); // full timestamp
        $transID = $this->conf->get("paycard_id");
        $identifier = $this->refnum($transID); 

        $validResponse = 1;
        $errorMsg = $xml->query('/RStream/CmdResponse/TextResponse');
        $balance = $xml->query('/RStream/TranResponse/Amount/Balance');
        $tranType = $xml->query('/RStream/TranResponse/TranType');
        $status = $xml->query('/RStream/CmdResponse/CmdStatus');
        $invoice = $xml->query('/RStream/TranResponse/InvoiceNo');
        $normalized = $this->getNormalized($status);
        $resultCode = ($normalized >= 3) ? 0 : $normalized;
        $rMsg = $normalized === 3 ? substr($errorMsg, 0, 100) : $status;
        $apprNumber = $xml->query('/RStream/TranResponse/RefNo');

        $finishQ = sprintf("UPDATE PaycardTransactions SET
                                responseDatetime='%s',
                                seconds=%f,
                                commErr=%d,
                                httpCode=%d,
                                validResponse=%d,
                                xResultCode=%d,
                                xApprovalNumber='%s',
                                xResponseCode=%d,
                                xResultMessage='%s',
                                xTransactionID='%s',
                                xBalance=%.2f
                            WHERE paycardTransactionID=%d",
                                $now,
                                $authResult['curlTime'],
                                $authResult['curlErr'],
                                $authResult['curlHTTP'],
                                $normalized,
                                $resultCode,
                                $apprNumber,
                                $resultCode,
                                $rMsg,
                                $apprNumber,
                                $balance,
                                $this->last_paycard_transaction_id
        );
        $dbTrans->query($finishQ);

        // check for communication errors (any cURL error or any HTTP code besides 200)
        if ($authResult['curlErr'] != CURLE_OK || $authResult['curlHTTP'] != 200) {
            if ($authResult['curlHTTP'] == '0') {
                if (!$this->secondTry) {
                    $this->secondTry = true;
                    return $this->sendAuth("w2.backuppay.com");
                }
                $this->conf->set("boxMsg","No response from processor<br />The transaction did not go through");
                return PaycardLib::PAYCARD_ERR_PROC;
            }

            return $this->setErrorMsg(PaycardLib::PAYCARD_ERR_COMM);
        }

         // check for data errors (any failure to parse response XML or echo'd field mismatch
        if ($validResponse != 1) {
            // invalid server response, we don't know if the transaction was processed (use carbon)
            return $this->setErrorMsg(PaycardLib::PAYCARD_ERR_DATA); 
        }

        // put the parsed response into session so the caller, receipt printer, etc can get the data they need
        $this->conf->set("paycard_response",array(
            'Balance' => strlen($balance) > 0 ? $balance : 0,
        ));
        /**
          Update authorized amount based on response. If
          the transaction was a refund ("Return") then the
          amount needs to be negative for POS to handle
          it correctly.
        */
        if ($normalized == 1) {
            $amt = $xml->query('/RStream/TranResponse/Amount/Authorize');
            $tranCode = $xml->query('/RStream/TranResponse/TranCode');
            $realAmt = $tranCode == 'Return' ? -1*$amt : $amt;
            if ($realAmt != $this->conf->get('paycard_amount')) {
                $correctionQ = sprintf("UPDATE PaycardTransactions SET amount=%f WHERE
                    dateID=%s AND refNum='%s'",
                    $amt,date("Ymd"),$identifier);
                $dbTrans->query($correctionQ);
                $this->conf->set('paycard_amount', $realAmt);
            }
        }

        // comm successful, check the Authorized, AuthorizationCode and ErrorMsg fields
        if ($status == 'Approved' && $apprNumber != '') {
            return PaycardLib::PAYCARD_ERR_OK; // authorization approved, no error
        }

        /**
         * For strange reasons the gateway sometimes declines encrypted prepaid
         * transactions but includes the full card number in the response. Until
         * this gets correct, we can just re-submit the transaction using the
         * decrypted card number. This only applies to gift (i.e., non-PCI) cards.
         */
        if ($status == 'Declined' && $this->conf->get('paycard_type') == PaycardLib::PAYCARD_TYPE_ENCRYPTED_GIFT) {
            $realPAN = $xml->query('/RStream/TranResponse/AcctNo');
            if (strlen($realPAN) == 19) {
                $this->conf->set('paycard_type', PaycardLib::PAYCARD_TYPE_GIFT);
                $this->conf->set('paycard_PAN', $realPAN);
                return $this->sendAuth();
            }
        }

        // the authorizor gave us some failure code
        // authorization failed, response fields in $_SESSION["paycard_response"]
        $this->conf->set("boxMsg","Processor error: ".$errorMsg);

        return PaycardLib::PAYCARD_ERR_PROC; 
    }

    private function handleResponseVoid($vdResult)
    {
        $resp = $this->desoapify("GiftTransactionResult",
            $vdResult["response"]);
        $xml = new BetterXmlData($resp);

        // initialize
        $dbTrans = Database::tDataConnect();

        // prepare data for the void request
        $now = date('Y-m-d H:i:s'); // full timestamp

        $validResponse = 1;
        $errorMsg = $xml->query('/RStream/CmdResponse/TextResponse');
        $balance = $xml->query('/RStream/TranResponse/Amount/Balance');
        $tranType = $xml->query('/RStream/TranResponse/TranType');
        $status = $xml->query('/RStream/CmdResponse/CmdStatus');
        $invoice = $xml->query('/RStream/TranResponse/InvoiceNo');
        $normalized = $this->getNormalized($status);
        $resultCode = ($normalized >= 3) ? 0 : $normalized;
        $rMsg = $normalized === 3 ? substr($errorMsg, 0, 100) : $status;
        $apprNumber = $xml->query('/RStream/TranResponse/RefNo');

        $finishQ = sprintf("UPDATE PaycardTransactions SET
                                responseDatetime='%s',
                                seconds=%f,
                                curlErr=%d,
                                httpCode=%d,
                                validResponse=%d,
                                xResultCode=%d,
                                xApprovalNumber='%s',
                                xResponseCode=%d,
                                xResultMessage='%s',
                                xTransactionID='%s',
                                xBalance=%.2f
                            WHERE paycardTransactionID=%d",
                                $now,
                                $vdResult['curlTime'],
                                $vdResult['curlErr'],
                                $vdResult['curlHTTP'],
                                $normalized,
                                $resultCode,
                                $apprNumber,
                                $resultCode,
                                $rMsg,
                                $apprNumber,
                                $balance,
                                $this->last_paycard_transaction_id
        );
        $dbTrans->query($finishQ);

        if ($vdResult['curlErr'] != CURLE_OK || $vdResult['curlHTTP'] != 200) {
            if ($vdResult['curlHTTP'] == '0'){
                if (!$this->secondTry){
                    $this->secondTry = true;
                    return $this->sendVoid("w2.backuppay.com");
                }
                $this->conf->set("boxMsg","No response from processor<br />The transaction did not go through");
                return PaycardLib::PAYCARD_ERR_PROC;
            }
            return $this->setErrorMsg(PaycardLib::PAYCARD_ERR_COMM); // comm error, try again
        }

        // check for data errors (any failure to parse response XML or echo'd field mismatch)
        if ($validResponse != 1) {
            // invalid server response, we don't know if the transaction was voided (use carbon)
            return $this->setErrorMsg(PaycardLib::PAYCARD_ERR_DATA);
        }

        // put the parsed response into session so the caller, receipt printer, etc can get the data they need
        $this->conf->set("paycard_response",array(
            'Balance' => strlen($balance) > 0 ? $balance : 0,
        ));

        // comm successful, check the Authorized, AuthorizationCode and ErrorMsg fields
        if ($status == 'Approved' && $apprNumber != '') {
            return PaycardLib::PAYCARD_ERR_OK; // void successful, no error
        }

        /**
         * For strange reasons the gateway sometimes declines encrypted prepaid
         * transactions but includes the full card number in the response. Until
         * this gets correct, we can just re-submit the transaction using the
         * decrypted card number. This only applies to gift (i.e., non-PCI) cards.
         */
        if ($status == 'Declined' && $this->conf->get('paycard_type') == PaycardLib::PAYCARD_TYPE_ENCRYPTED_GIFT) {
            $realPAN = $xml->query('/RStream/TranResponse/AcctNo');
            if (strlen($realPAN) == 19) {
                $this->conf->set('paycard_type', PaycardLib::PAYCARD_TYPE_GIFT);
                $this->conf->set('paycard_PAN', $realPAN);
                return $this->sendVoid();
            }
        }

        // the authorizor gave us some failure code
        $this->conf->set("boxMsg","PROCESSOR ERROR: " . $errorMsg);

        return PaycardLib::PAYCARD_ERR_PROC; 
    }

    private function handleResponseBalance($balResult)
    {
        $resp = $this->desoapify("GiftTransactionResult",
            $balResult["response"]);
        $xml = new BetterXmlData($resp);

        if ($balResult['curlErr'] != CURLE_OK || $balResult['curlHTTP'] != 200) {
            if ($balResult['curlHTTP'] == '0'){
                if (!$this->secondTry) {
                    $this->secondTry = true;
                    return $this->sendBalance("w2.backuppay.com");
                }
                $this->conf->set("boxMsg","No response from processor<br />The transaction did not go through");
                return PaycardLib::PAYCARD_ERR_PROC;
            }
            return $this->setErrorMsg(PaycardLib::PAYCARD_ERR_COMM); // comm error, try again
        }

        $this->conf->set("paycard_response",array());
        $resp = array();
        $balance = $xml->query('/RStream/TranResponse/Amount/Balance');
        if (strlen($balance) > 0) {
            $resp["Balance"] = $balance;
            $this->conf->set("paycard_response",$resp);
        }

        $tranType = $xml->query('/RStream/TranResponse/TranType');
        $cmdStatus = $xml->query('/RStream/CmdResponse/CmdStatus');
        if ($tranType == 'PrePaid' && $cmdStatus == 'Approved') {
            return PaycardLib::PAYCARD_ERR_OK; // balance checked, no error
        }

        /**
         * For strange reasons the gateway sometimes declines encrypted prepaid
         * transactions but includes the full card number in the response. Until
         * this gets correct, we can just re-submit the transaction using the
         * decrypted card number. This only applies to gift (i.e., non-PCI) cards.
         */
        if ($cmdStatus == 'Declined' && $this->conf->get('paycard_type') == PaycardLib::PAYCARD_TYPE_ENCRYPTED_GIFT) {
            $realPAN = $xml->query('/RStream/TranResponse/AcctNo');
            if (strlen($realPAN) == 19) {
                $this->conf->set('paycard_type', PaycardLib::PAYCARD_TYPE_GIFT);
                $this->conf->set('paycard_PAN', $realPAN);
                return $this->sendBalance();
            }
        }

        // the authorizor gave us some failure code
        $textResponse = $xml->query('/RStream/CmdResponse/TextResponse');
        $this->conf->set("boxMsg","Processor error: ". $textResponse);

        return PaycardLib::PAYCARD_ERR_PROC;
    }

    private function getTermID()
    {
        if ($this->conf->get("training") == 1) {
            return '019588466313922';
        }
        return $this->conf->get('MercuryGiftID');
    }

    private function getPw()
    {
        if ($this->conf->get("training") == 1) {
            return "xyz";
        }
        return $this->conf->get('MercuryGiftPassword');
    }

    private function getPAN()
    {
        if ($this->conf->get("training") == 1) {
            return "6050110000000296951";
        }
        return $this->conf->get("paycard_PAN");
    }

    private function getTrack2()
    {
        if ($this->conf->get("training") == 1) {
            return false;
        }
        return $this->conf->get("paycard_tr2");
    }

    /**
     * @return string XML
     *   - Encryption fields if PAN is P2PE block
     *   - Otherwise Track 2 if present
     *   - Otherwise PAN as account number
     */
    private function getAccount()
    {
        $pan = $this->getPAN();
        $tr2 = $this->getTrack2();
        if (substr($pan, 0, 8) == "02AA0080") {
            $encBlock = new COREPOS\pos\plugins\Paycards\card\EncBlock();
            $e2e = $encBlock->parseEncBlock($this->conf->get("paycard_PAN"));
            return <<<XML
<EncryptedFormat>{$e2e['Format']}</EncryptedFormat>
<AccountSource>Swiped</AccountSource>
<EncryptedBlock>{$e2e['Block']}</EncryptedBlock>
<EncryptedKey>{$e2e['Key']}</EncryptedKey>
XML;
        } elseif ($tr2) {
            return "<Track2>{$tr2}</Track2>";
        }

        return "<AcctNo>{$pan}</AcctNo>";
    }

    private function getNormalized($status)
    {
        if ($status === 'Approved') {
            return 1;
        } elseif ($status === 'Declined') {
            return 2;
        } elseif ($status === 'Error') {
            return 3;
        }
        return 4;
    }
}