/**
  * handleGatewayResponse
  * This action should be used when a gateway submits a POST/GET response
  * for which we need to action. In this case, the PayPal IPN. We shall
  * return void as nothing is returned from this method. It is not public
  * facing and is present to handle system to system communications over 
  * HTTP communications only. If the gateway doesn't support POST/GET type
  * responses, implement the back office order updating within the
  * newPaymentSuccess() method instead.
  *
  * ATTRIBUTION
  * Snippets of IPN code were used from PayPal's GitHub samples on 15-10-2015.
  * https://github.com/paypal/ipn-code-samples/blob/master/paypal_ipn.php
  * @author PayPal 
  * 
  * @param SS_HTTPRequest $request The GET/POST variables and URL parameters.
  * @return Void
  */
 public function handleGatewayResponse($request)
 {
     /**
      * Only proceed if we have postVars set
      */
     if ($request->postVars()) {
         $gateway = DataObject::get_one("Gateway_PayPal");
         $debug = $gateway->Debug;
         /**
          * STEP ONE 
          * Prepend cmd=_notify-validate to the POST request from PayPal.
          * Reading posted data direction from $request->postVars() may
          * cause serialization isusues with array data. We therefore
          * will read directly from the input stream instead.
          */
         $raw_post_data = file_get_contents('php://input');
         $raw_post_array = explode('&', $raw_post_data);
         $myPost = array();
         foreach ($raw_post_array as $keyval) {
             $keyval = explode('=', $keyval);
             if (count($keyval) == 2) {
                 $myPost[$keyval[0]] = urldecode($keyval[1]);
             }
         }
         $req = 'cmd=_notify-validate';
         if (function_exists('get_magic_quotes_gpc')) {
             $get_magic_quotes_exists = true;
         }
         foreach ($myPost as $key => $value) {
             if ($get_magic_quotes_exists == true && get_magic_quotes_gpc() == 1) {
                 $value = urlencode(stripslashes($value));
             } else {
                 $value = urlencode($value);
             }
             $req .= "&{$key}={$value}";
         }
         /**
          * STEP TWO
          * Which PayPal URL are we dealing with?
          */
         if (DataObject::get_one("Gateway_PayPal")->Sandbox) {
             $paypal_url = "https://www.sandbox.paypal.com/cgi-bin/webscr";
         } else {
             $paypal_url = "https://www.paypal.com/cgi-bin/webscr";
         }
         /**
          * STEP THREE
          * Initiate curl IPN callback to post IPN data back to PayPal
          * to validate the IPN data is genuine. Without this step anyone
          * can fake IPN data and mess with your order system.
          */
         $ch = curl_init($paypal_url);
         if ($ch == FALSE) {
             return FALSE;
         }
         /* Set curl Options */
         curl_setopt($ch, CURLOPT_HTTP_VERSION, CURL_HTTP_VERSION_1_1);
         curl_setopt($ch, CURLOPT_POST, 1);
         curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
         curl_setopt($ch, CURLOPT_POSTFIELDS, $req);
         curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, 1);
         curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, 2);
         curl_setopt($ch, CURLOPT_FORBID_REUSE, 1);
         /* Set TCP timeout to 30 seconds */
         curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 30);
         curl_setopt($ch, CURLOPT_HTTPHEADER, array('Connection: Close'));
         /* Execute Curl and Store Response in $res */
         $res = curl_exec($ch);
         /* Are there curl errors? If yes, log them if Debug is enabled. */
         if (curl_errno($ch) != 0) {
             if ($debug == 1) {
                 $this->newLogEntry("Can't connect to PayPal to validate IPN message: " . curl_error($ch));
             }
             curl_close($ch);
             exit;
             /* No errors */
         } else {
             /* If Debug is enabled, save to the log. */
             if ($debug == 1) {
                 $this->newLogEntry("HTTP request of validation request" . curl_getinfo($ch, CURLINFO_HEADER_OUT) . " for IPN payload: {$req}");
                 $this->newLogEntry("HTTP response of validation request: {$res}");
             }
             curl_close($ch);
         }
         /**
          * STEP FOUR
          * Inspect IPN validation result and act accordingly.
          * 1 - Split response headers and payload, a better way for strcmp.
          * 2 - Do the actions, based on response. 
          */
         $tokens = explode("\r\n\r\n", trim($res));
         $res = trim(end($tokens));
         if (strcmp($res, "VERIFIED") == 0) {
             /**
              * DEBUG
              * If debug is enabled, log the details
              * of this IPN response. 
              */
             if ($debug) {
                 $this->newLogEntry("Verified IPN: {$req} ");
             }
             /**
              * ERROR CHECK 1
              * txn_type must be of type 'web_accept'. (Buy Now Button) 
              */
             if (!$request->postVar("txn_type") == "web_accept") {
                 if ($debug == 1) {
                     $this->newLogEntry("ERROR: (txn_id: " . $request->postVar("txn_id") . ") txn_type is not of type 'web_accept'.");
                 }
                 return Store_Controller::create()->httpError(400);
                 exit;
             }
             /** 
              * ERROR CHECK 2
              * We must be the intended recipient for the transaction. 
              */
             if ($gateway->EmailAddress != $request->postVar("receiver_email")) {
                 if ($debug == 1) {
                     $this->newLogEntry("ERROR: (txn_id: " . $request->postVar("txn_id") . ") Intended recipient " . "(" . $request->postVar("receiver_email") . ") does not " . "match that set in the gateway settings.");
                 }
                 return Store_Controller::create()->httpError(400);
                 exit;
             }
             /**
              * ERROR CHECK 3
              * An order related to this payment must exist. 
              */
             $order = new SQLQuery("COUNT(*)");
             $order->setFrom("`order`")->addWhere("(`id`='" . $request->postVar("custom") . "')");
             if ($order->execute()->value() < 1) {
                 if ($debug == 1) {
                     $this->newLogEntry("ERROR: (txn_id: " . $request->postVar("txn_id") . ") The order number defined in 'custom' " . "(" . $request->postVar("custom") . ") does not exist in the system.");
                 }
                 return Store_Controller::create()->httpError(400);
                 exit;
             }
             /**
              * ERROR CHECK 4
              * This IPN message can not be a duplicate. 
              */
             $dup = new SQLQuery("COUNT(*)");
             $dup->setFrom("`Order_Payment_PayPal`");
             $dup->addWhere("(`txn_id`='" . $request->postVar("txn_id") . "') AND (`payment_status`='" . $request->postVar("payment_status") . "')");
             $dup_count = $dup->execute()->value();
             if ($dup_count > 0) {
                 if ($debug == 1) {
                     $this->newLogEntry("ERROR: (txn_id: " . $request->postVar("txn_id") . ") The IPN message received is a duplicate of one " . "previously received.");
                 }
                 return Store_Controller::create()->httpError(400);
                 exit;
             }
             /** 
              * ERROR CHECK 5
              * The mc_gross has to match the total order price. 
              */
             $order_total = DataObject::get_by_id("Order", $request->postVar("custom"))->calculateOrderTotal();
             $mc_gross = $request->postVar("mc_gross");
             if ($order_total != $mc_gross) {
                 if ($debug == 1) {
                     $this->newLogEntry("ERROR: (txn_id: " . $request->postVar("txn_id") . ") The payment amount did not match the order amount.");
                 }
                 return Store_Controller::create()->httpError(400);
                 exit;
             }
             /**
              * ERROR CHECK 6
              * If this IPN is not a duplicate, are there
              * any other entries for this txn_id?
              */
             if ($dup_count < 1) {
                 /* Count how many entries there are with the IPNs txn_id */
                 $record_count = new SQLQuery("COUNT(*)");
                 $record_count->setFrom("Order_Payment_PayPal");
                 $record_count->addWhere("(`txn_id`='" . $request->postVar("txn_id") . "')");
                 $record_count = $record_count->execute()->value();
                 /* The row ID for the record that was found, if one exists */
                 $payment_record_id = new SQLQuery("`id`");
                 $payment_record_id->setFrom("Order_Payment_PayPal")->addWhere("(`txn_id`='" . $request->postVar("txn_id") . "')");
                 $payment_record_id = $payment_record_id->execute()->value();
             }
             /**
              * VERIFIED STEP ONE 
              * 
              * Either create a payment record or update an existing one an send the applicable emails.
              */
             switch ($request->postVar("payment_status")) {
                 /* Payment has cleared, order can progress. */
                 case "Completed":
                     //Send email to admin notification email address
                     Order_Emails::create()->adminNewOrderNotification($request->postVar("custom"));
                     //Send email to the customer confirming their order, if they haven't had one already.
                     Order_Emails::create()->customerNewOrderConfirmation($request->postVar("custom"));
                     if ($record_count > 0) {
                         $this->updatePaymentRecord($request, $payment_record_id, "Completed", "Processing");
                     } else {
                         $this->newPaymentRecord($request, "Completed", "Processing");
                     }
                     break;
                     /* The payment is pending. See pending_reason for more information.	*/
                 /* The payment is pending. See pending_reason for more information.	*/
                 case "Pending":
                     /**
                      * We don't send emails for this status as 'Pending' orders are still awaiting a response from
                      * a payment gateway and should not be dispatched. It is safe to send a confirmation email to
                      * the customer, however.
                      */
                     //Send email to the customer confirming their order is currently pending
                     Order_Emails::create()->customerNewOrderConfirmation($request->postVar("custom"), "Pending");
                     if ($record_count > 0) {
                         $this->updatePaymentRecord($request, $payment_record_id, "Pending", "Pending / Awaiting Payment");
                     } else {
                         $this->newPaymentRecord($request, "Pending", "Pending / Awaiting Payment");
                     }
                     break;
                     /* You refunded the payment. */
                 /* You refunded the payment. */
                 case "Refunded":
                     /* Notify the customer of a change to their order status */
                     Order_Emails::create()->customerOrderStatusUpdate($request->postVar("custom"), "Refunded");
                     if ($record_count > 0) {
                         $this->updatePaymentRecord($request, $payment_record_id, "Refunded", "Refunded");
                     } else {
                         $this->newPaymentRecord($request, "Refunded", "Refunded");
                     }
                     break;
                     /**
                      * A payment was reversed due to a chargeback or other type of reversal.
                      * The funds have been removed from your account balance and returned to the buyer.
                      * The reason for the reversal is specified in the ReasonCode element.
                      */
                 /**
                  * A payment was reversed due to a chargeback or other type of reversal.
                  * The funds have been removed from your account balance and returned to the buyer.
                  * The reason for the reversal is specified in the ReasonCode element.
                  */
                 case "Reversed":
                     /* Notify the admin that an order has had an order has been reversed */
                     /* Notify the customer of a change to their order status */
                     Order_Emails::create()->customerOrderStatusUpdate($request->postVar("custom"), "Cancelled");
                     if ($record_count > 0) {
                         $this->updatePaymentRecord($request, $payment_record_id, "Refunded", "Cancelled");
                     } else {
                         $this->newPaymentRecord($request, "Refunded", "Cancelled");
                     }
                     break;
                     /* The reveral was cancelled */
                 /* The reveral was cancelled */
                 case "Canceled_Reversal":
                     /* Notify an admin that an order reversal has been cancelled */
                     /**
                      * We don't send customers an email update for this status as it might
                      * cause confustion.
                      */
                     /**
                      * For canceled reversals, lets set the order to Pending as an admin will need to manually review it.
                      * we don't want it to fall in the standard Processing queue as goods could be shipped twice.
                      */
                     if ($record_count > 0) {
                         $this->updatePaymentRecord($request, $payment_record_id, "Pending", "Pending / Awaiting Payment");
                     } else {
                         $this->newPaymentRecord($request, "Pending", "Pending / Awaiting Payment");
                     }
                     break;
                     /* This authorization has been voided. */
                 /* This authorization has been voided. */
                 case "Voided":
                     /* Notify the customer of a change to their order status */
                     Order_Emails::create()->customerOrderStatusUpdate($request->postVar("custom"), "Cancelled");
                     if ($record_count > 0) {
                         $this->updatePaymentRecord($request, $payment_record_id, "Refunded", "Cancelled");
                     } else {
                         $this->newPaymentRecord($request, "Refunded", "Cancelled");
                     }
                     break;
                     /**
                      * The payment has failed.
                      */
                 /**
                  * The payment has failed.
                  */
                 case "Failed":
                     /* Notify the customer of a change to their order status */
                     Order_Emails::create()->customerOrderStatusUpdate($request->postVar("custom"), "Cancelled");
                     if ($record_count > 0) {
                         $this->updatePaymentRecord($request, $payment_record_id, "Refunded", "Cancelled");
                     } else {
                         $this->newPaymentRecord($request, "Refunded", "Cancelled");
                     }
                     break;
                     /* Other IPN statuses are ignored. */
                 /* Other IPN statuses are ignored. */
                 default:
                     exit;
                     break;
             }
         } elseif (strcmp($res, "INVALID") == 0) {
             $status = "INVALID";
             // log for manual investigation
             // Add business logic here which deals with invalid IPN messages
             /* If Debug is enabled, log response */
             if ($debug == 1) {
                 error_log(date('[Y-m-d H:i e] ') . "Invalid IPN: {$req}" . PHP_EOL, 3, "../ipn.log");
             }
         }
     }
 }
 public function test()
 {
     Order_Emails::create()->customerOrderStatusUpdate("74", "Cancelled");
 }