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