Source of file PayPalExpressCheckoutPayment.php
Size: 26,641 Bytes - Last Modified: 2021-12-23T10:45:54+00:00
/var/www/docs.ssmods.com/process/src/code/PayPalExpressCheckoutPayment.php
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593 | <?php /** * PayPal Express Checkout Payment * @author Jeremy Shipman jeremy [at] burnbright.net * @author Nicolaas [at] sunnysideup.co.nz * * Developer documentation: * Integration guide: https://cms.paypal.com/cms_content/US/en_US/files/developer/PP_ExpressCheckout_IntegrationGuide.pdf * API reference: https://cms.paypal.com/us/cgi-bin/?cmd=_render-content&content_ID=developer/howto_api_reference * Uses the Name-Value Pair API protocol * */ class PayPalExpressCheckoutPayment extends EcommercePayment { private static $debug = false; private static $db = array( 'Token' => 'Varchar(30)', 'PayerID' => 'Varchar(30)', 'TransactionID' => 'Varchar(30)', 'AuthorisationCode' => 'Text', 'Debug' => 'HTMLText' ); private static $logo = "ecommerce/images/paymentmethods/paypal.jpg"; private static $payment_methods = array(); //PayPal URLs private static $test_API_Endpoint = "https://api-3t.sandbox.paypal.com/nvp"; private static $test_PAYPAL_URL = "https://www.sandbox.paypal.com/webscr?cmd=_express-checkout&token="; private static $API_Endpoint = "https://api-3t.paypal.com/nvp"; private static $PAYPAL_URL = "https://www.paypal.com/cgi-bin/webscr?cmd=_express-checkout&token="; private static $privacy_link = "https://www.paypal.com/us/cgi-bin/webscr?cmd=p/gen/ua/policy_privacy-outside"; //config private static $test_mode = true; //on by default private static $API_UserName; private static $API_Password; private static $API_Signature; private static $sBNCode = null; // BN Code is only applicable for partners private static $version = '64'; //set custom settings private static $custom_settings = array( //design //'HDRIMG' => "http://www.mysite.com/images/logo.jpg", //max size = 750px wide by 90px high, and good to be on secure server //'HDRBORDERCOLOR' => 'CCCCCC', //header border //'HDRBACKCOLOR' => '00FFFF', //header background //'PAYFLOWCOLOR'=> 'AAAAAA' //payflow colour //'PAGESTYLE' => //page style set in merchant account settings 'SOLUTIONTYPE' => 'Sole'//require paypal account, or not. Can be or 'Mark' (required) or 'Sole' (not required) //'BRANDNAME' => 'my site name'//override business name in checkout //'CUSTOMERSERVICENUMBER' => '0800 1234 5689'//number to call to resolve payment issues //'NOSHIPPING' => 1 //disable showing shipping details ); public function getCMSFields() { $fields = parent::getCMSFields(); foreach (array_keys(self::$db) as $field) { $fields->removeFieldFromTab('Root.Main', $field); $fields->addFieldToTab('Root.Advanced', LiteralField::create($field.'_debug', '<h2>'.$field.'</h2><pre>'.$this->$field.'</pre>')); } return $fields; } public function getPaymentFormFields($amount = 0, $order = NULL) { $logo = '<img src="' . $this->Config()->get("logo") . '" alt="Credit card payments powered by PayPal"/>'; $privacyLink = '<a href="' . $this->Config()->get("privacy_link") . '" target="_blank" title="Read PayPal\'s privacy policy">' . $logo . '</a><br/>'; return new FieldList( new LiteralField('PayPalInfo', $privacyLink), new LiteralField( 'PayPalPaymentsList', $this->renderWith("PaymentMethods") ) ); } public function getPaymentFormRequirements() { return null; } //main processing function public function processPayment($data, $form) { //sanity checks for credentials if (!$this->Config()->get("API_UserName") || !$this->Config()->get("API_Password") || !$this->Config()->get("API_Signature")) { user_error('You are attempting to make a payment without the necessary credentials set', E_USER_ERROR); } $data = $this->Order()->BillingAddress()->toMap(); $paymenturl = $this->getTokenURL($this->Amount->Amount, $this->Amount->Currency, $data); $this->Status = "Incomplete"; $this->write(); if ($paymenturl) { Controller::curr()->redirect($paymenturl); //redirect to payment gateway return EcommercePayment_Processing::create(); } $this->Message = _t('PayPalExpressCheckoutPayment.COULDNOTBECONTACTED', "PayPal could not be contacted"); $this->Status = 'Failure'; $this->write(); return EcommercePayment_Failure::create($this->Message); } /** * * depracated */ public function PayPalForm() { user_error("This form is no longer used."); Requirements::javascript(THIRDPARTY_DIR . '/jquery/jquery.js'); // 1) Main Information $fields = ''; $order = $this->Order(); $items = $order->Items(); $member = $order->Member(); // 2) Main Settings $url = $this->Config()->get("test_mode") ? $this->Config()->get("test_url") : $this->Config()->get("url"); $inputs['cmd'] = '_cart'; $inputs['upload'] = '1'; // 3) Items Informations $cpt = 0; foreach ($items as $item) { $inputs['item_name_' . ++$cpt] = $item->TableTitle(); // item_number is unnecessary $inputs['amount_' . $cpt] = $item->UnitPrice(); $inputs['quantity_' . $cpt] = $item->Quantity; } // 4) Payment Informations And Authorisation Code $inputs['business'] = $this->Config()->get("test_mode") ? $this->Config()->get("test_account_email") : $this->Config()->get("account_email"); $inputs['custom'] = $this->ID . '-' . $this->AuthorisationCode; // Add Here The Shipping And/Or Taxes $inputs['currency_code'] = $this->Currency; // 5) Redirection Informations $inputs['cancel_return'] = Director::absoluteBaseURL() . PayPalExpressCheckoutPayment_Handler::cancel_link($inputs['custom']); $inputs['return'] = Director::absoluteBaseURL() . PayPalExpressCheckoutPayment_Handler::complete_link(); $inputs['rm'] = '2'; // Add Here The Notify URL // 6) PayPal Pages Style Optional Informations if (self:: $continue_button_text) { $inputs['cbt'] = $this->Config()->get("continue_button_text"); } if ($this->Config()->get("header_image_url")) { $inputs['cpp_header_image'] = urlencode($this->Config()->get("header_image_url")); } if ($this->Config()->get("header_back_color")) { $inputs['cpp_headerback_color'] = $this->Config()->get("header_back_color"); } if ($this->Config()->get("header_border_color")) { $inputs['cpp_headerborder_color'] = $this->Config()->get("header_border_color"); } if ($this->Config()->get("payflow_color")) { $inputs['cpp_payflow_color'] = $this->Config()->get("payflow_color"); } if ($this->Config()->get("back_color")) { $inputs['cs'] = $this->Config()->get("back_color"); } if ($this->Config()->get("image_url")) { $inputs['image_url'] = urlencode($this->Config()->get("image_url")); } if ($this->Config()->get("page_style")) { $inputs['page_style'] = $this->Config()->get("page_style"); } // 7) Prepopulating Customer Informations $billingAddress = $order->BillingAddress(); $inputs['first_name'] = $billingAddress->FirstName; $inputs['last_name'] = $billingAddress->Surname; $inputs['address1'] = $billingAddress->Address; $inputs['address2'] = $billingAddress->Address2; $inputs['city'] = $billingAddress->City; $inputs['zip'] = $billingAddress->PostalCode; $inputs['state'] = $billingAddress->Region()->Code; $inputs['country'] = $billingAddress->Country; $inputs['email'] = $member->Email; // 8) Form Creation if (is_array($inputs) && count($inputs)) { foreach ($inputs as $name => $value) { $ATT_value = Convert::raw2att($value); $fields .= "<input type=\"hidden\" name=\"$name\" value=\"$ATT_value\" />"; } } return <<<HTML <form id="PaymentForm" method="post" action="$url"> $fields <input type="submit" value="Submit" /> </form> <script type="text/javascript"> jQuery(document).ready(function() { jQuery("input[type='submit']").hide(); jQuery('#PaymentForm').submit(); }); </script> HTML; } public function populateDefaults() { parent::populateDefaults(); $this->AuthorisationCode = md5(uniqid(rand(), true)); } /** * Requests a Token url, based on the provided Name-Value-Pair fields * See docs for more detail on these fields: * https://cms.paypal.com/us/cgi-bin/?cmd=_render-content&content_ID=developer/e_howto_api_nvp_r_SetExpressCheckout * * Note: some of these values will override the paypal merchant account settings. * Note: not all fields are listed here. */ protected function getTokenURL($paymentAmount, $currencyCodeType, $extradata = array()) { $data = array( //payment info 'PAYMENTREQUEST_0_AMT' => $paymentAmount, 'PAYMENTREQUEST_0_CURRENCYCODE' => $currencyCodeType, //TODO: check to be sure all currency codes match the SS ones //TODO: include individual costs: shipping, shipping discount, insurance, handling, tax?? //'PAYMENTREQUEST_0_ITEMAMT' => //item(s) //'PAYMENTREQUEST_0_SHIPPINGAMT' //shipping //'PAYMENTREQUEST_0_SHIPDISCAMT' //shipping discount //'PAYMENTREQUEST_0_HANDLINGAMT' //handling //'PAYMENTREQUEST_0_TAXAMT' //tax //'PAYMENTREQUEST_0_INVNUM' => $this->PaidObjectID //invoice number //'PAYMENTREQUEST_0_TRANSACTIONID' //Transaction id //'PAYMENTREQUEST_0_DESC' => //description //'PAYMENTREQUEST_0_NOTETEXT' => //note to merchant //'PAYMENTREQUEST_0_PAYMENTACTION' => , //Sale, Order, or Authorization //'PAYMENTREQUEST_0_PAYMENTREQUESTID' //return urls 'RETURNURL' => PayPalExpressCheckoutPayment_Handler::return_link(), 'CANCELURL' => PayPalExpressCheckoutPayment_Handler::cancel_link(), //'PAYMENTREQUEST_0_NOTIFYURL' => //Instant payment notification //'CALLBACK' //'CALLBACKTIMEOUT' //shipping display //'REQCONFIRMSHIPPING' //require that paypal account address be confirmed 'NOSHIPPING' => 1, //show shipping fields, or not 0 = show shipping, 1 = don't show shipping, 2 = use account address, if none passed //'ALLOWOVERRIDE' //display only the provided address, not the one stored in paypal //TODO: Probably overkill, but you can even include the prices,qty,weight,tax etc for individual sale items //other settings //'LOCALECODE' => //locale, or default to US 'LANDINGPAGE' => 'Billing' //can be 'Billing' or 'Login' ); if (!isset($extradata['Name'])) { $arr = array(); if (isset($extradata['FirstName'])) { $arr[] = $extradata['FirstName']; } if (isset($extradata['MiddleName'])) { $arr[] = $extradata['MiddleName']; } if (isset($extradata['Surname'])) { $arr[] = $extradata['Surname']; } $extradata['Name'] = implode(' ', $arr); } $extradata["OrderID"] = SiteConfig::current_site_config()->Title." ".$this->Order()->getTitle(); //add member & shipping fields, etc ...this will pre-populate the paypal login / create account form foreach (array( 'Email' => 'EMAIL', 'Name' => 'PAYMENTREQUEST_0_SHIPTONAME', 'Address' => 'PAYMENTREQUEST_0_SHIPTOSTREET', 'Address2' => 'PAYMENTREQUEST_0_SHIPTOSTREET2', 'City' => 'PAYMENTREQUEST_0_SHIPTOCITY', 'PostalCode' => 'PAYMENTREQUEST_0_SHIPTOZIP', 'Region' => 'PAYMENTREQUEST_0_SHIPTOPHONENUM', 'Phone' => 'PAYMENTREQUEST_0_SHIPTOPHONENUM', 'Country' => 'PAYMENTREQUEST_0_SHIPTOCOUNTRYCODE', 'OrderID' => 'PAYMENTREQUEST_0_DESC' ) as $field => $val) { if (isset($extradata[$field])) { $data[$val] = $extradata[$field]; } elseif ($this->$field) { $data[$val] = $this->$field; } } //set design settings $data = array_merge($this->Config()->get("custom_settings"), $data); $response = $this->apiCall('SetExpressCheckout', $data); if (Config::inst()->get("PayPalExpressCheckoutPayment", "debug")) { $this->addDebugInfo("RESPONSE: ".print_r($response, 1)); $debugmessage = "PayPal Debug:" . "\nMode: $mode". "\nAPI url: ".$this->getApiEndpoint(). "\nRedirect url: ".$this->getPayPalURL($response['TOKEN']). "\nUsername: " .$this->Config()->get("API_UserName"). "\nPassword: " .$this->Config()->get("API_Password"). "\nSignature: ".$this->Config()->get("API_Signature"). "\nRequest Data: ".print_r($data, true). "\nResponse: ".print_r($response, true); $this->addDebugInfo("DEBUG MESSAGE: ".$debugmessage); } if (!isset($response['ACK']) || !(strtoupper($response['ACK']) == "SUCCESS" || strtoupper($response['ACK']) == "SUCCESSWITHWARNING")) { $mode = ($this->Config()->get("test_mode") === true) ? "test" : "live"; return null; } //get and save token for later $token = $response['TOKEN']; $this->Token = $token; $this->write(); return $this->getPayPalURL($token); } /** * see https://cms.paypal.com/us/cgi-bin/?cmd=_render-content&content_ID=developer/e_howto_api_nvp_r_DoExpressCheckoutPayment */ public function confirmPayment() { $data = array( 'PAYERID' => $this->PayerID, 'TOKEN' => $this->Token, 'PAYMENTREQUEST_0_PAYMENTACTION' => "Sale", 'PAYMENTREQUEST_0_AMT' => $this->Amount->Amount, 'PAYMENTREQUEST_0_CURRENCYCODE' => $this->Amount->Currency, 'IPADDRESS' => urlencode($_SERVER['SERVER_NAME']) ); $response = $this->apiCall('DoExpressCheckoutPayment', $data); if (!isset($response['ACK']) || !(strtoupper($response['ACK']) == "SUCCESS" || strtoupper($response['ACK']) == "SUCCESSWITHWARNING")) { return null; } if (isset($response["PAYMENTINFO_0_TRANSACTIONID"])) { //' Unique transaction ID of the payment. Note: If the PaymentAction of the request was Authorization or Order, this value is your AuthorizationID for use with the Authorization & Capture APIs. $this->TransactionID = $response["PAYMENTINFO_0_TRANSACTIONID"]; } //$transactionType = $response["PAYMENTINFO_0_TRANSACTIONTYPE"]; //' The type of transaction Possible values: l cart l express-checkout //$paymentType = $response["PAYMENTTYPE"]; //' Indicates whether the payment is instant or delayed. Possible values: l none l echeck l instant //$orderTime = $response["ORDERTIME"]; //' Time/date stamp of payment //TODO: should these be updated like this? //$this->Amount->Amount = $response["AMT"]; //' The final amount charged, including any shipping and taxes from your Merchant Profile. //$this->Amount->Currency= $response["CURRENCYCODE"]; //' A three-character currency code for one of the currencies listed in PayPay-Supported Transactional Currencies. Default: USD. //TODO: store this extra info locally? //$feeAmt = $response["FEEAMT"]; //' PayPal fee amount charged for the transaction //$settleAmt = $response["SETTLEAMT"]; //' Amount deposited in your PayPal account after a currency conversion. //$taxAmt = $response["TAXAMT"]; //' Tax charged on the transaction. //$exchangeRate = $response["EXCHANGERATE"]; //' Exchange rate if a currency conversion occurred. Relevant only if your are billing in their non-primary currency. If the customer chooses to pay with a currency other than the non-primary currency, the conversion occurs in the customer's account. if (isset($response["PAYMENTINFO_0_PAYMENTSTATUS"])) { switch (strtoupper($response["PAYMENTINFO_0_PAYMENTSTATUS"])) { case "PROCESSED": case "COMPLETED": $this->Status = 'Success'; $this->Message = _t('PayPalExpressCheckoutPayment.SUCCESS', "The payment has been completed, and the funds have been successfully transferred"); break; case "EXPIRED": $this->Message = _t('PayPalExpressCheckoutPayment.AUTHORISATION', "The authorization period for this payment has been reached"); $this->Status = 'Failure'; break; case "DENIED": $this->Message = _t('PayPalExpressCheckoutPayment.FAILURE', "Payment was denied"); $this->Status = 'Failure'; break; case "REVERSED": $this->Status = 'Failure'; break; case "VOIDED": $this->Message = _t('PayPalExpressCheckoutPayment.VOIDED', "An authorization for this transaction has been voided."); $this->Status = 'Failure'; break; case "FAILED": $this->Status = 'Failure'; break; case "CANCEL-REVERSAL": // A reversal has been canceled; for example, when you win a dispute and the funds for the reversal have been returned to you. break; case "IN-PROGRESS": $this->Message = _t('PayPalExpressCheckoutPayment.INPROGRESS', "The transaction has not terminated");//, e.g. an authorization may be awaiting completion."; break; case "PARTIALLY-REFUNDED": $this->Message = _t('PayPalExpressCheckoutPayment.PARTIALLYREFUNDED', "The payment has been partially refunded."); break; case "PENDING": $this->Message = _t('PayPalExpressCheckoutPayment.PENDING', "The payment is pending."); if (isset($response["PAYMENTINFO_0_PENDINGREASON"])) { $this->Message .= " ".$this->getPendingReason($response["PAYMENTINFO_0_PENDINGREASON"]); } break; case "REFUNDED": $this->Message = _t('PayPalExpressCheckoutPayment.REFUNDED', "Payment refunded."); break; default: } } //$reasonCode = $response["REASONCODE"]; $this->write(); } protected function getPendingReason($reason) { switch ($reason) { case "address": return _t('PayPalExpressCheckoutPayment.PENDING.ADDRESS', "A confirmed shipping address was not provided."); case "authorization": return _t('PayPalExpressCheckoutPayment.PENDING.AUTHORISATION', "Payment has been authorised, but not settled."); case "echeck": return _t('PayPalExpressCheckoutPayment.PENDING.ECHECK', "eCheck has not cleared."); case "intl": return _t('PayPalExpressCheckoutPayment.PENDING.INTERNATIONAL', "International: payment must be accepted or denied manually."); case "multicurrency": return _t('PayPalExpressCheckoutPayment.PENDING.MULTICURRENCY', "Multi-currency: payment must be accepted or denied manually."); case "order": case "paymentreview": case "unilateral": case "verify": case "other": } } /** * Handles actual communication with API server. */ protected function apiCall($method, $data = array()) { $this->addDebugInfo('---------------------------------------'); $this->addDebugInfo('---------------------------------------'); $this->addDebugInfo('---------------------------------------'); $postfields = array( 'METHOD' => $method, 'VERSION' => $this->Config()->get("version"), 'USER' => $this->Config()->get("API_UserName"), 'PWD'=> $this->Config()->get("API_Password"), 'SIGNATURE' => $this->Config()->get("API_Signature"), 'BUTTONSOURCE' => $this->Config()->get("sBNCode") ); if (Config::inst()->get("PayPalExpressCheckoutPayment", "debug")) { $this->addDebugInfo("STANDARD POSTING FIELDS .... //// : ".print_r($postfields, 1)); $this->addDebugInfo("ADDITIONAL POSTING FIELDS .... //// : ".print_r($data, 1)); $this->addDebugInfo("SENDING TO .... //// : ".print_r($this->getApiEndpoint(), 1)); } $postfields = array_merge($postfields, $data); //Make POST request to Paypal via RESTful service $rs = new RestfulService($this->getApiEndpoint(), 0); //REST connection that will expire immediately $rs->httpHeader('Accept: application/xml'); $rs->httpHeader('Content-Type: application/x-www-form-urlencoded'); $response = $rs->request('', 'POST', http_build_query($postfields)); if (Config::inst()->get("PayPalExpressCheckoutPayment", "debug")) { $this->addDebugInfo('RESPONSE .... //// : '.print_r($response, 1)); } return $this->deformatNVP($response->getBody()); } protected function deformatNVP($nvpstr) { $intial = 0; $nvpArray = array(); while (strlen($nvpstr)) { //postion of Key $keypos= strpos($nvpstr, '='); //position of value $valuepos = strpos($nvpstr, '&') ? strpos($nvpstr, '&'): strlen($nvpstr); /*getting the Key and Value values and storing in a Associative Array*/ $keyval=substr($nvpstr, $intial, $keypos); $valval=substr($nvpstr, $keypos+1, $valuepos-$keypos-1); //decoding the respose $nvpArray[urldecode($keyval)] =urldecode($valval); $nvpstr=substr($nvpstr, $valuepos+1, strlen($nvpstr)); } return $nvpArray; } protected function getApiEndpoint() { return ($this->Config()->get("test_mode") === true) ? $this->Config()->get("test_API_Endpoint") : $this->Config()->get("API_Endpoint"); } protected function getPayPalURL($token) { $url = ($this->Config()->get("test_mode") === true) ? $this->Config()->get("test_PAYPAL_URL") : $this->Config()->get("PAYPAL_URL"); return $url.$token.'&useraction=commit'; //useraction=commit ensures the payment is confirmed on PayPal, and not on a merchant confirm page. } protected function addDebugInfo($msg) { $this->Debug .= "---------//------------\n\n".$msg; $this->write(); } } /** * Handler for responses from the PayPal site */ class PayPalExpressCheckoutPayment_Handler extends Controller { private static $url_segment = 'paypalexpresscheckoutpayment_handler'; protected $payment = null; //only need to get this once private static $allowed_actions = array( 'confirm', 'cancel' ); public function Link($action = null) { return Controller::join_links( Director::baseURL(), $this->Config()->get("url_segment"), $action ); } public function payment() { if ($this->payment) { return $this->payment; } elseif ($token = Controller::getRequest()->getVar('token')) { $payment = PayPalExpressCheckoutPayment::get() ->filter( array( "Token" => $token, "Status" => "Incomplete" ) ) ->first(); $this->payment = $payment; $this->payment->init(); return $this->payment; } return null; } public function confirm($request) { //TODO: pretend the user confirmed, and skip straight to results. (check that this is allowed) //TODO: get updated shipping details from paypal?? if ($payment = $this->payment()) { if ($pid = Controller::getRequest()->getVar('PayerID')) { $payment->PayerID = $pid; $payment->write(); $payment->confirmPayment(); } } else { //something went wrong? ..perhaps trying to pay for a payment that has already been processed } $this->doRedirect(); return; } public function cancel($request) { if ($payment = $this->payment()) { //TODO: do API call to gather further information $payment->Status = "Failure"; $payment->Message = _t('PayPalExpressCheckoutPayment.USERCANCELLED', "User cancelled"); $payment->write(); } $this->doRedirect(); return; } protected function doRedirect() { $payment = $this->payment(); if ($payment && $obj = $payment->PaidObject()) { $this->redirect($obj->Link()); return; } $this->redirect(Director::absoluteURL('home', true)); //TODO: make this customisable in Payment_Controllers return; } public static function return_link() { return Director::absoluteURL(Config::inst()->get("PayPalExpressCheckoutPayment_Handler", "url_segment"), true)."/confirm/"; } public static function cancel_link() { return Director::absoluteURL(Config::inst()->get("PayPalExpressCheckoutPayment_Handler", "url_segment"), true)."/cancel/"; } } |