/libs/verysimple/Payment/PayPal.php

http://github.com/jasonhinkle/phreeze · PHP · 409 lines · 203 code · 70 blank · 136 comment · 33 complexity · 2e259dc3b59306824139d7d7a4fa20b5 MD5 · raw file

  1. <?php
  2. /** @package verysimple::Payment */
  3. /** import supporting libraries */
  4. require_once("PaymentProcessor.php");
  5. /**
  6. * PayPal extends the generic PaymentProcessor object to process
  7. * a PaymentRequest through the PayPal direct payment API.
  8. *
  9. * @package verysimple::Payment
  10. * @author VerySimple Inc.
  11. * @copyright 1997-2012 VerySimple, Inc.
  12. * @license http://www.gnu.org/licenses/lgpl.html LGPL
  13. * @version 2.1
  14. */
  15. class PayPal extends PaymentProcessor
  16. {
  17. // used by paypal - 'sandbox' or 'beta-sandbox' or 'live'
  18. private $environment = 'sandbox';
  19. /**
  20. * Called on contruction
  21. * @param bool $test set to true to enable test mode. default = false
  22. */
  23. function Init($testmode)
  24. {
  25. // set the post url depending on whether we're in test mode or not
  26. $this->environment = $testmode ? 'sandbox' : 'live';
  27. }
  28. /**
  29. * @see PaymentProcessor::Refund()
  30. */
  31. function Refund(RefundRequest $req)
  32. {
  33. $resp = new PaymentResponse();
  34. $resp->OrderNumber = $req->InvoiceId;
  35. $nvpStr = "&TRANSACTIONID=" . urlencode($req->TransactionId);
  36. $nvpStr .= "&CURRENCYCODE=" . urlencode($req->TransactionCurrency);
  37. if($req->RefundType == RefundRequest::$REFUND_TYPE_PARTIAL)
  38. {
  39. if (!$req->RefundAmount)
  40. {
  41. $resp->IsSuccess = false;
  42. $resp->ResponseMessage = "RefundAmount is required for partial refund";
  43. return $resp;
  44. }
  45. $nvpStr .= "&REFUNDTYPE=Partial&AMT=" . urldecode($req->RefundAmount);
  46. }
  47. else
  48. {
  49. $nvpStr .= "&REFUNDTYPE=Full";
  50. }
  51. if ($req->Memo) $nvpStr .= "&NOTE=" . urlencode($req->Memo);
  52. if ($req->InvoiceId) $nvpStr .= "&INVOICEID=" . urlencode($req->InvoiceId);
  53. // Execute the API operation
  54. $resp->RawResponse = $this->PPHttpPost('RefundTransaction', $nvpStr);
  55. if("SUCCESS" == strtoupper($resp->RawResponse["ACK"]) || "SUCCESSWITHWARNING" == strtoupper($resp->RawResponse["ACK"]))
  56. {
  57. /* SAMPLE SUCCESS RESPONSE
  58. Array (
  59. [REFUNDTRANSACTIONID] => 5L51568382268602F
  60. [FEEREFUNDAMT] => 0%2e18
  61. [GROSSREFUNDAMT] => 5%2e15
  62. [NETREFUNDAMT] => 4%2e97
  63. [CURRENCYCODE] => USD
  64. [TIMESTAMP] => 2012%2d03%2d22T23%3a25%3a25Z
  65. [CORRELATIONID] => 2a2cc13c7c64a
  66. [ACK] => Success
  67. [VERSION] => 62%2e0
  68. [BUILD] => 2649250
  69. )
  70. */
  71. $resp->IsSuccess = true;
  72. $resp->TransactionId = $resp->RawResponse['REFUNDTRANSACTIONID'];
  73. $resp->OrderNumber = $req->InvoiceId;
  74. $resp->ResponseMessage = urldecode($resp->RawResponse['GROSSREFUNDAMT'] . ' ' . $resp->RawResponse['CURRENCYCODE']) . " was sucessfully refunded.";
  75. }
  76. else
  77. {
  78. /* SAMPLE ERROR RESPONSE:
  79. Array (
  80. [TIMESTAMP] => 2012%2d03%2d22T23%3a15%3a22Z
  81. [CORRELATIONID] => ae19bab2b4b1
  82. [ACK] => Failure [VERSION] => 62%2e0
  83. [BUILD] => 2649250
  84. [L_ERRORCODE0] => 10004
  85. [L_SHORTMESSAGE0] => Transaction%20refused%20because%20of%20an%20invalid%20argument%2e%20See%20additional%20error%20messages%20for%20details%2e
  86. [L_LONGMESSAGE0] => The%20transaction%20id%20is%20not%20valid
  87. [L_SEVERITYCODE0] => Error
  88. )
  89. */
  90. $resp->IsSuccess = false;
  91. $resp->ResponseCode = urldecode( $this->GetArrayVal($resp->RawResponse,"L_ERRORCODE0") );
  92. $resp->ResponseMessage = $this->GetErrorMessage($resp->RawResponse);
  93. }
  94. $resp->ParsedResponse = "";
  95. $delim = "";
  96. foreach (array_keys($resp->RawResponse) as $key)
  97. {
  98. $resp->ParsedResponse .= $delim . $key . "='" . urldecode( $resp->RawResponse[$key]) . "'";
  99. $delim = ", ";
  100. }
  101. return $resp;
  102. }
  103. /**
  104. * Process a PaymentRequest
  105. * @param PaymentRequest $req Request object to be processed
  106. * @return PaymentResponse
  107. */
  108. function Process(PaymentRequest $req)
  109. {
  110. $resp = new PaymentResponse();
  111. $resp->OrderNumber = $req->OrderNumber;
  112. // before bothering with contacting the processor, check for some basic fields
  113. if ($req->CCNumber == '')
  114. {
  115. $resp->IsSuccess = false;
  116. $resp->ResponseCode = "0";
  117. $resp->ResponseMessage = "No Credit Card Number Provided";
  118. return $resp;
  119. }
  120. if ($req->CustomerFirstName == '')
  121. {
  122. $resp->IsSuccess = false;
  123. $resp->ResponseCode = "0";
  124. $resp->ResponseMessage = "No Cardholder Name Provided";
  125. return $resp;
  126. }
  127. // post to paypal service
  128. // Set request-specific fields.
  129. $paymentType = $req->TransactionType == PaymentRequest::$TRANSACTION_TYPE_AUTH_CAPTURE
  130. ? urlencode('Sale')
  131. : urlencode('Authorization') ;
  132. $firstName = urlencode($req->CustomerFirstName);
  133. $lastName = urlencode($req->CustomerLastName);
  134. $creditCardType = urlencode( trim($req->CCType) );
  135. $creditCardNumber = urlencode( trim($req->CCNumber) );
  136. // month needs to be two digits - padded with leading zero if necessary
  137. $padDateMonth = urlencode( trim(str_pad($req->CCExpMonth, 2, '0', STR_PAD_LEFT)) );
  138. // year needs to be full 4-digit
  139. $expDateYear = urlencode( $this->GetFullYear( trim($req->CCExpYear) ) );
  140. $email = (urlencode($req->CustomerEmail));
  141. $invoiceNum = (urlencode($req->InvoiceNumber));
  142. $orderDesc = (urlencode($req->OrderDescription));
  143. $cvv2Number = urlencode($req->CCSecurityCode);
  144. $address1 = urlencode($req->CustomerStreetAddress);
  145. $address2 = urlencode($req->CustomerStreetAddress2);
  146. $city = urlencode($req->CustomerCity);
  147. $state = urlencode($req->CustomerState);
  148. $zip = urlencode($req->CustomerZipCode);
  149. $amount = urlencode($req->TransactionAmount);
  150. $currencyID = urlencode($req->TransactionCurrency); // or other currency ('GBP', 'EUR', 'JPY', 'CAD', 'AUD')
  151. // soft descriptor can only be a max of 22 chars with no non-alphanumeric characters
  152. $softdescriptor = urlencode(substr(preg_replace("/[^a-zA-Z0-9\s]/", "", $req->SoftDescriptor),0,22 ));
  153. // legit country code list: https://cms.paypal.com/us/cgi-bin/?&cmd=_render-content&content_ID=developer/e_howto_api_nvp_country_codes
  154. $country = urlencode( strtoupper($req->CustomerCountry) ); // US or other valid country code
  155. if ($country == "USA") $country = "US";
  156. // Add request-specific fields to the request string.
  157. $nvpStr = "&PAYMENTACTION=$paymentType&AMT=$amount&CREDITCARDTYPE=$creditCardType&ACCT=$creditCardNumber".
  158. "&EXPDATE=$padDateMonth$expDateYear&CVV2=$cvv2Number&FIRSTNAME=$firstName&LASTNAME=$lastName".
  159. "&STREET=$address1&CITY=$city&STATE=$state&ZIP=$zip&COUNTRYCODE=$country&CURRENCYCODE=$currencyID".
  160. "&DESC=$orderDesc&INVNUM=$invoiceNum&EMAIL=$email&SOFTDESCRIPTOR=$softdescriptor";
  161. // make the post - use a try/catch in case of network errors
  162. try
  163. {
  164. $resp->RawResponse = $this->PPHttpPost('DoDirectPayment', $nvpStr);
  165. if ("SUCCESS" == strtoupper($resp->RawResponse["ACK"]) || "SUCCESSWITHWARNING" == strtoupper($resp->RawResponse["ACK"]))
  166. {
  167. $resp->IsSuccess = true;
  168. $resp->TransactionId = urldecode( $this->GetArrayVal($resp->RawResponse,"TRANSACTIONID") );
  169. $resp->ResponseCode = urldecode( "AVSCODE=" . $this->GetArrayVal($resp->RawResponse,"AVSCODE") . ",CVV2MATCH=" . $this->GetArrayVal($resp->RawResponse,"CVV2MATCH"));
  170. $resp->ResponseMessage = urldecode( "Charge of " . $this->GetArrayVal($resp->RawResponse,"AMT") . " Posted" );
  171. }
  172. else
  173. {
  174. // for error descriptions, refrer to https://cms.paypal.com/us/cgi-bin/?cmd=_render-content&content_ID=developer/e_howto_api_nvp_errorcodes
  175. $resp->IsSuccess = false;
  176. $resp->ResponseCode = urldecode( $this->GetArrayVal($resp->RawResponse,"L_ERRORCODE0") );
  177. $resp->ResponseMessage = $this->GetErrorMessage($resp->RawResponse);
  178. }
  179. $resp->ParsedResponse = "";
  180. $delim = "";
  181. foreach (array_keys($resp->RawResponse) as $key)
  182. {
  183. $resp->ParsedResponse .= $delim . $key . "='" . urldecode( $resp->RawResponse[$key]) . "'";
  184. $delim = ", ";
  185. }
  186. }
  187. catch (Exception $ex)
  188. {
  189. // this means we had a connection error talking to the gateway
  190. $resp->IsSuccess = false;
  191. $resp->ResponseCode = $ex->getCode();
  192. $resp->ResponseMessage = $ex->getMessage();
  193. }
  194. return $resp;
  195. }
  196. /**
  197. * Given a response array, attempt to return the most sensible error message.
  198. * @param array $httpParsedResponseAr
  199. */
  200. private function GetErrorMessage($httpParsedResponseAr)
  201. {
  202. /* SAMPLE RETURN VALUE
  203. (
  204. [TIMESTAMP] => 2012%2d03%2d23T01%3a47%3a01Z
  205. [CORRELATIONID] => c9dcc0b7acd94
  206. [ACK] => Failure
  207. [VERSION] => 62%2e0
  208. [BUILD] => 2649250
  209. [L_ERRORCODE0] => 10507
  210. [L_SHORTMESSAGE0] => Invalid%20Configuration
  211. [L_LONGMESSAGE0] => This%20transaction%20cannot%20be%20processed%2e%20Please%20contact%20PayPal%20Customer%20Service%2e
  212. [L_SEVERITYCODE0] => Error
  213. [AMT] => 103%2e00
  214. [CURRENCYCODE] => USD
  215. )
  216. */
  217. $errmsg = $this->GetArrayVal($httpParsedResponseAr,"L_SHORTMESSAGE0") . ": ";
  218. // figure out the message as PayPal reports it
  219. $longmessage = $this->GetArrayVal($httpParsedResponseAr,"L_LONGMESSAGE0");
  220. // paypal prepends this to every message, so strip it out
  221. $longmessage = str_replace("This%20transaction%20cannot%20be%20processed%2e","", $longmessage );
  222. if ($longmessage != "")
  223. {
  224. // this will generally be the best description of the error
  225. $errmsg .= $longmessage;
  226. }
  227. else
  228. {
  229. // paypal didn't give a simple error description so we have to try to decipher the gateway response
  230. // this is the response code from the gateway
  231. $processor_code = $this->GetArrayVal($httpParsedResponseAr,"L_ERRORPARAMVALUE0");
  232. if ($processor_code)
  233. {
  234. // this will usually be "ProcessorResponse" in which case we don't need to display it
  235. $processor_prefix = $this->GetArrayVal($httpParsedResponseAr,"L_ERRORPARAMID0");
  236. $processor_prefix = ($processor_prefix == "ProcessorResponse")
  237. ? ''
  238. : $processor_prefix . ' - ';
  239. $processor_message = $processor_prefix . $this->getProcessorResponseDescription($processor_code);
  240. $errmsg .= $processor_message;
  241. }
  242. }
  243. return urldecode($errmsg);
  244. }
  245. /**
  246. * Util to return array values without throwing an undefined error
  247. *
  248. * @param Array $arr
  249. * @param variant $key
  250. * @param variant $not_defined_val value to return if array key doesn't exist
  251. * @return variant
  252. */
  253. private function GetArrayVal($arr,$key,$not_defined_val = "")
  254. {
  255. return array_key_exists($key,$arr)
  256. ? $arr[$key]
  257. : $not_defined_val;
  258. }
  259. /**
  260. * Send HTTP POST Request. Throws an Exception if the server communication failed
  261. *
  262. * @param string The API method name
  263. * @param string The POST Message fields in &name=value pair format
  264. * @return array Parsed HTTP Response body
  265. */
  266. private function PPHttpPost($methodName_, $nvpStr_) {
  267. // Set up your API credentials, PayPal end point, and API version.
  268. $API_UserName = urlencode($this->Username);
  269. $API_Password = urlencode($this->Password);
  270. $API_Signature = urlencode($this->Signature);
  271. $API_Endpoint = "https://api-3t.paypal.com/nvp";
  272. if ("sandbox" === $this->environment || "beta-sandbox" === $this->environment)
  273. {
  274. $API_Endpoint = "https://api-3t." . $this->environment . ".paypal.com/nvp";
  275. }
  276. //$version = urlencode('51.0');
  277. $version = urlencode('62.0');
  278. // Set the curl parameters.
  279. $ch = curl_init();
  280. curl_setopt($ch, CURLOPT_URL, $API_Endpoint);
  281. curl_setopt($ch, CURLOPT_VERBOSE, 1);
  282. // Turn off the server and peer verification (TrustManager Concept).
  283. curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, FALSE);
  284. curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, FALSE);
  285. curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
  286. curl_setopt($ch, CURLOPT_POST, 1);
  287. // Set the API operation, version, and API signature in the request.
  288. $nvpreq = "METHOD=$methodName_&VERSION=$version&PWD=$API_Password&USER=$API_UserName&SIGNATURE=$API_Signature$nvpStr_";
  289. // Set the request as a POST FIELD for curl.
  290. curl_setopt($ch, CURLOPT_POSTFIELDS, $nvpreq);
  291. // Get response from the server.
  292. $httpResponse = curl_exec($ch);
  293. if(!$httpResponse) {
  294. throw new Exception("$methodName_ failed: ".curl_error($ch).'('.curl_errno($ch).')');
  295. }
  296. // Extract the response details.
  297. $httpResponseAr = explode("&", $httpResponse);
  298. $httpParsedResponseAr = array();
  299. foreach ($httpResponseAr as $i => $value) {
  300. $tmpAr = explode("=", $value);
  301. if(sizeof($tmpAr) > 1) {
  302. $httpParsedResponseAr[$tmpAr[0]] = $tmpAr[1];
  303. }
  304. }
  305. if((0 == sizeof($httpParsedResponseAr)) || !array_key_exists('ACK', $httpParsedResponseAr)) {
  306. throw new Exception("Invalid HTTP Response for POST request($nvpreq) to $API_Endpoint.");
  307. }
  308. return $httpParsedResponseAr;
  309. }
  310. /**
  311. * This returns a formatted error given a payment processor error response code.
  312. *
  313. * @link https://www.x.com/blogs/matt/2010/10/26/error-codes-explained-15005
  314. * @param string $code
  315. * @return string possible description of error
  316. */
  317. private function getProcessorResponseDescription($code)
  318. {
  319. return "Transaction was rejected by the issuing bank with error code $code.";
  320. // @TODO these have proven to be unreliable, but maybe somebody can do something better?
  321. /*
  322. $responses = Array();
  323. $responses['0005'] = "The transaction was declined without explanation by the card issuer.";
  324. $responses['0013'] = "The transaction amount is greater than the maximum the issuer allows.";
  325. $responses['0014'] = "The issuer indicates that this card is not valid.";
  326. $responses['0051'] = "The credit limit for this account has been exceeded.";
  327. $responses['0054'] = "The card is expired.";
  328. $responses['1015'] = "The credit card number was invalid.";
  329. $responses['1511'] = "Duplicate transaction attempt.";
  330. $responses['1899'] = "Timeout waiting for host response.";
  331. $responses['2075'] = "Approval from the card issuer's voice center is required to process this transaction.";
  332. return array_key_exists($code,$responses)
  333. ? "Error Code " . $code . ": " .$responses[$code]
  334. : "Error Code " . $code;
  335. */
  336. }
  337. }
  338. ?>