[ Index ] |
PHP Cross Reference of Phabricator |
[Summary view] [Print] [Text view]
1 <?php 2 3 final class PhortunePayPalPaymentProvider extends PhortunePaymentProvider { 4 5 const PAYPAL_API_USERNAME = 'paypal.api-username'; 6 const PAYPAL_API_PASSWORD = 'paypal.api-password'; 7 const PAYPAL_API_SIGNATURE = 'paypal.api-signature'; 8 const PAYPAL_MODE = 'paypal.mode'; 9 10 public function isAcceptingLivePayments() { 11 $mode = $this->getProviderConfig()->getMetadataValue(self::PAYPAL_MODE); 12 return ($mode === 'live'); 13 } 14 15 public function getName() { 16 return pht('PayPal'); 17 } 18 19 public function getConfigureName() { 20 return pht('Add PayPal Payments Account'); 21 } 22 23 public function getConfigureDescription() { 24 return pht( 25 'Allows you to accept various payment instruments with a paypal.com '. 26 'account.'); 27 } 28 29 public function getConfigureProvidesDescription() { 30 return pht( 31 'This merchant accepts payments via PayPal.'); 32 } 33 34 public function getConfigureInstructions() { 35 return pht( 36 "To configure PayPal, register or log into an existing account on ". 37 "[[https://paypal.com | paypal.com]] (for live payments) or ". 38 "[[https://sandbox.paypal.com | sandbox.paypal.com]] (for test ". 39 "payments). Once logged in:\n\n". 40 " - Navigate to {nav Tools > API Access}.\n". 41 " - Choose **View API Signature**.\n". 42 " - Copy the **API Username**, **API Password** and **Signature** ". 43 " into the fields above.\n\n". 44 "You can select whether the provider operates in test mode or ". 45 "accepts live payments using the **Mode** dropdown above.\n\n". 46 "You can either use `sandbox.paypal.com` to retrieve live credentials, ". 47 "or `paypal.com` to retrieve live credentials."); 48 } 49 50 public function getAllConfigurableProperties() { 51 return array( 52 self::PAYPAL_API_USERNAME, 53 self::PAYPAL_API_PASSWORD, 54 self::PAYPAL_API_SIGNATURE, 55 self::PAYPAL_MODE, 56 ); 57 } 58 59 public function getAllConfigurableSecretProperties() { 60 return array( 61 self::PAYPAL_API_PASSWORD, 62 self::PAYPAL_API_SIGNATURE, 63 ); 64 } 65 66 public function processEditForm( 67 AphrontRequest $request, 68 array $values) { 69 70 $errors = array(); 71 $issues = array(); 72 73 if (!strlen($values[self::PAYPAL_API_USERNAME])) { 74 $errors[] = pht('PayPal API Username is required.'); 75 $issues[self::PAYPAL_API_USERNAME] = pht('Required'); 76 } 77 78 if (!strlen($values[self::PAYPAL_API_PASSWORD])) { 79 $errors[] = pht('PayPal API Password is required.'); 80 $issues[self::PAYPAL_API_PASSWORD] = pht('Required'); 81 } 82 83 if (!strlen($values[self::PAYPAL_API_SIGNATURE])) { 84 $errors[] = pht('PayPal API Signature is required.'); 85 $issues[self::PAYPAL_API_SIGNATURE] = pht('Required'); 86 } 87 88 if (!strlen($values[self::PAYPAL_MODE])) { 89 $errors[] = pht('Mode is required.'); 90 $issues[self::PAYPAL_MODE] = pht('Required'); 91 } 92 93 return array($errors, $issues, $values); 94 } 95 96 public function extendEditForm( 97 AphrontRequest $request, 98 AphrontFormView $form, 99 array $values, 100 array $issues) { 101 102 $form 103 ->appendChild( 104 id(new AphrontFormTextControl()) 105 ->setName(self::PAYPAL_API_USERNAME) 106 ->setValue($values[self::PAYPAL_API_USERNAME]) 107 ->setError(idx($issues, self::PAYPAL_API_USERNAME, true)) 108 ->setLabel(pht('Paypal API Username'))) 109 ->appendChild( 110 id(new AphrontFormTextControl()) 111 ->setName(self::PAYPAL_API_PASSWORD) 112 ->setValue($values[self::PAYPAL_API_PASSWORD]) 113 ->setError(idx($issues, self::PAYPAL_API_PASSWORD, true)) 114 ->setLabel(pht('Paypal API Password'))) 115 ->appendChild( 116 id(new AphrontFormTextControl()) 117 ->setName(self::PAYPAL_API_SIGNATURE) 118 ->setValue($values[self::PAYPAL_API_SIGNATURE]) 119 ->setError(idx($issues, self::PAYPAL_API_SIGNATURE, true)) 120 ->setLabel(pht('Paypal API Signature'))) 121 ->appendChild( 122 id(new AphrontFormSelectControl()) 123 ->setName(self::PAYPAL_MODE) 124 ->setValue($values[self::PAYPAL_MODE]) 125 ->setError(idx($issues, self::PAYPAL_MODE)) 126 ->setLabel(pht('Mode')) 127 ->setOptions( 128 array( 129 'test' => pht('Test Mode'), 130 'live' => pht('Live Mode'), 131 ))); 132 133 return; 134 } 135 136 public function canRunConfigurationTest() { 137 return true; 138 } 139 140 public function runConfigurationTest() { 141 $result = $this 142 ->newPaypalAPICall() 143 ->setRawPayPalQuery('GetBalance', array()) 144 ->resolve(); 145 } 146 147 public function getPaymentMethodDescription() { 148 return pht('Credit Card or PayPal Account'); 149 } 150 151 public function getPaymentMethodIcon() { 152 return 'PayPal'; 153 } 154 155 public function getPaymentMethodProviderDescription() { 156 return 'PayPal'; 157 } 158 159 protected function executeCharge( 160 PhortunePaymentMethod $payment_method, 161 PhortuneCharge $charge) { 162 throw new Exception('!'); 163 } 164 165 protected function executeRefund( 166 PhortuneCharge $charge, 167 PhortuneCharge $refund) { 168 169 $transaction_id = $charge->getMetadataValue('paypal.transactionID'); 170 if (!$transaction_id) { 171 throw new Exception(pht('Charge has no transaction ID!')); 172 } 173 174 $refund_amount = $refund->getAmountAsCurrency()->negate(); 175 $refund_currency = $refund_amount->getCurrency(); 176 $refund_value = $refund_amount->formatBareValue(); 177 178 $params = array( 179 'TRANSACTIONID' => $transaction_id, 180 'REFUNDTYPE' => 'Partial', 181 'AMT' => $refund_value, 182 'CURRENCYCODE' => $refund_currency, 183 ); 184 185 $result = $this 186 ->newPaypalAPICall() 187 ->setRawPayPalQuery('RefundTransaction', $params) 188 ->resolve(); 189 190 $charge->setMetadataValue( 191 'paypal.refundID', 192 $result['REFUNDTRANSACTIONID']); 193 } 194 195 public function updateCharge(PhortuneCharge $charge) { 196 $transaction_id = $charge->getMetadataValue('paypal.transactionID'); 197 if (!$transaction_id) { 198 throw new Exception(pht('Charge has no transaction ID!')); 199 } 200 201 $params = array( 202 'TRANSACTIONID' => $transaction_id, 203 ); 204 205 $result = $this 206 ->newPaypalAPICall() 207 ->setRawPayPalQuery('GetTransactionDetails', $params) 208 ->resolve(); 209 210 $is_charge = false; 211 $is_fail = false; 212 switch ($result['PAYMENTSTATUS']) { 213 case 'Processed': 214 case 'Completed': 215 case 'Completed-Funds-Held': 216 $is_charge = true; 217 break; 218 case 'Partially-Refunded': 219 case 'Refunded': 220 case 'Reversed': 221 case 'Canceled-Reversal': 222 // TODO: Handle these. 223 return; 224 case 'In-Progress': 225 case 'Pending': 226 // TODO: Also handle these better? 227 return; 228 case 'Denied': 229 case 'Expired': 230 case 'Failed': 231 case 'None': 232 case 'Voided': 233 default: 234 $is_fail = true; 235 break; 236 } 237 238 if ($charge->getStatus() == PhortuneCharge::STATUS_HOLD) { 239 $cart = $charge->getCart(); 240 241 $unguarded = AphrontWriteGuard::beginScopedUnguardedWrites(); 242 if ($is_charge) { 243 $cart->didApplyCharge($charge); 244 } else if ($is_fail) { 245 $cart->didFailCharge($charge); 246 } 247 unset($unguarded); 248 } 249 } 250 251 private function getPaypalAPIUsername() { 252 return $this 253 ->getProviderConfig() 254 ->getMetadataValue(self::PAYPAL_API_USERNAME); 255 } 256 257 private function getPaypalAPIPassword() { 258 return $this 259 ->getProviderConfig() 260 ->getMetadataValue(self::PAYPAL_API_PASSWORD); 261 } 262 263 private function getPaypalAPISignature() { 264 return $this 265 ->getProviderConfig() 266 ->getMetadataValue(self::PAYPAL_API_SIGNATURE); 267 } 268 269 /* -( One-Time Payments )-------------------------------------------------- */ 270 271 public function canProcessOneTimePayments() { 272 return true; 273 } 274 275 /* -( Controllers )-------------------------------------------------------- */ 276 277 278 public function canRespondToControllerAction($action) { 279 switch ($action) { 280 case 'checkout': 281 case 'charge': 282 case 'cancel': 283 return true; 284 } 285 return parent::canRespondToControllerAction(); 286 } 287 288 public function processControllerRequest( 289 PhortuneProviderActionController $controller, 290 AphrontRequest $request) { 291 292 $viewer = $request->getUser(); 293 294 $cart = $controller->loadCart($request->getInt('cartID')); 295 if (!$cart) { 296 return new Aphront404Response(); 297 } 298 299 $charge = $controller->loadActiveCharge($cart); 300 switch ($controller->getAction()) { 301 case 'checkout': 302 if ($charge) { 303 throw new Exception(pht('Cart is already charging!')); 304 } 305 break; 306 case 'charge': 307 case 'cancel': 308 if (!$charge) { 309 throw new Exception(pht('Cart is not charging yet!')); 310 } 311 break; 312 } 313 314 switch ($controller->getAction()) { 315 case 'checkout': 316 $return_uri = $this->getControllerURI( 317 'charge', 318 array( 319 'cartID' => $cart->getID(), 320 )); 321 322 $cancel_uri = $this->getControllerURI( 323 'cancel', 324 array( 325 'cartID' => $cart->getID(), 326 )); 327 328 $price = $cart->getTotalPriceAsCurrency(); 329 330 $charge = $cart->willApplyCharge($viewer, $this); 331 332 $params = array( 333 'PAYMENTREQUEST_0_AMT' => $price->formatBareValue(), 334 'PAYMENTREQUEST_0_CURRENCYCODE' => $price->getCurrency(), 335 'PAYMENTREQUEST_0_PAYMENTACTION' => 'Sale', 336 'PAYMENTREQUEST_0_CUSTOM' => $charge->getPHID(), 337 'PAYMENTREQUEST_0_DESC' => $cart->getName(), 338 339 'RETURNURL' => $return_uri, 340 'CANCELURL' => $cancel_uri, 341 342 // TODO: This should be cart-dependent if we eventually support 343 // physical goods. 344 'NOSHIPPING' => '1', 345 ); 346 347 $result = $this 348 ->newPaypalAPICall() 349 ->setRawPayPalQuery('SetExpressCheckout', $params) 350 ->resolve(); 351 352 $uri = new PhutilURI('https://www.sandbox.paypal.com/cgi-bin/webscr'); 353 $uri->setQueryParams( 354 array( 355 'cmd' => '_express-checkout', 356 'token' => $result['TOKEN'], 357 )); 358 359 $cart->setMetadataValue('provider.checkoutURI', (string)$uri); 360 $cart->save(); 361 362 $charge->setMetadataValue('paypal.token', $result['TOKEN']); 363 $charge->save(); 364 365 return id(new AphrontRedirectResponse()) 366 ->setIsExternal(true) 367 ->setURI($uri); 368 case 'charge': 369 if ($cart->getStatus() !== PhortuneCart::STATUS_PURCHASING) { 370 return id(new AphrontRedirectResponse()) 371 ->setURI($cart->getCheckoutURI()); 372 } 373 374 $token = $request->getStr('token'); 375 376 $params = array( 377 'TOKEN' => $token, 378 ); 379 380 $result = $this 381 ->newPaypalAPICall() 382 ->setRawPayPalQuery('GetExpressCheckoutDetails', $params) 383 ->resolve(); 384 385 if ($result['CUSTOM'] !== $charge->getPHID()) { 386 throw new Exception( 387 pht('Paypal checkout does not match Phortune charge!')); 388 } 389 390 if ($result['CHECKOUTSTATUS'] !== 'PaymentActionNotInitiated') { 391 return $controller->newDialog() 392 ->setTitle(pht('Payment Already Processed')) 393 ->appendParagraph( 394 pht( 395 'The payment response for this charge attempt has already '. 396 'been processed.')) 397 ->addCancelButton($cart->getCheckoutURI(), pht('Continue')); 398 } 399 400 $price = $cart->getTotalPriceAsCurrency(); 401 402 $params = array( 403 'TOKEN' => $token, 404 'PAYERID' => $result['PAYERID'], 405 406 'PAYMENTREQUEST_0_AMT' => $price->formatBareValue(), 407 'PAYMENTREQUEST_0_CURRENCYCODE' => $price->getCurrency(), 408 'PAYMENTREQUEST_0_PAYMENTACTION' => 'Sale', 409 ); 410 411 $result = $this 412 ->newPaypalAPICall() 413 ->setRawPayPalQuery('DoExpressCheckoutPayment', $params) 414 ->resolve(); 415 416 $transaction_id = $result['PAYMENTINFO_0_TRANSACTIONID']; 417 418 $success = false; 419 $hold = false; 420 switch ($result['PAYMENTINFO_0_PAYMENTSTATUS']) { 421 case 'Processed': 422 case 'Completed': 423 case 'Completed-Funds-Held': 424 $success = true; 425 break; 426 case 'In-Progress': 427 case 'Pending': 428 // TODO: We can capture more information about this stuff. 429 $hold = true; 430 break; 431 case 'Denied': 432 case 'Expired': 433 case 'Failed': 434 case 'Partially-Refunded': 435 case 'Canceled-Reversal': 436 case 'None': 437 case 'Refunded': 438 case 'Reversed': 439 case 'Voided': 440 default: 441 // These are all failure states. 442 break; 443 } 444 445 $unguarded = AphrontWriteGuard::beginScopedUnguardedWrites(); 446 447 $charge->setMetadataValue('paypal.transactionID', $transaction_id); 448 $charge->save(); 449 450 if ($success) { 451 $cart->didApplyCharge($charge); 452 $response = id(new AphrontRedirectResponse())->setURI( 453 $cart->getCheckoutURI()); 454 } else if ($hold) { 455 $cart->didHoldCharge($charge); 456 457 $response = $controller 458 ->newDialog() 459 ->setTitle(pht('Charge On Hold')) 460 ->appendParagraph( 461 pht('Your charge is on hold, for reasons?')) 462 ->addCancelButton($cart->getCheckoutURI(), pht('Continue')); 463 } else { 464 $cart->didFailCharge($charge); 465 466 $response = $controller 467 ->newDialog() 468 ->setTitle(pht('Charge Failed')) 469 ->addCancelButton($cart->getCheckoutURI(), pht('Continue')); 470 } 471 unset($unguarded); 472 473 return $response; 474 case 'cancel': 475 if ($cart->getStatus() === PhortuneCart::STATUS_PURCHASING) { 476 $unguarded = AphrontWriteGuard::beginScopedUnguardedWrites(); 477 // TODO: Since the user cancelled this, we could conceivably just 478 // throw it away or make it more clear that it's a user cancel. 479 $cart->didFailCharge($charge); 480 unset($unguarded); 481 } 482 483 return id(new AphrontRedirectResponse()) 484 ->setURI($cart->getCheckoutURI()); 485 } 486 487 throw new Exception( 488 pht('Unsupported action "%s".', $controller->getAction())); 489 } 490 491 private function newPaypalAPICall() { 492 if ($this->isAcceptingLivePayments()) { 493 $host = 'https://api-3t.paypal.com/nvp'; 494 } else { 495 $host = 'https://api-3t.sandbox.paypal.com/nvp'; 496 } 497 498 return id(new PhutilPayPalAPIFuture()) 499 ->setHost($host) 500 ->setAPIUsername($this->getPaypalAPIUsername()) 501 ->setAPIPassword($this->getPaypalAPIPassword()) 502 ->setAPISignature($this->getPaypalAPISignature()); 503 } 504 505 506 }
title
Description
Body
title
Description
Body
title
Description
Body
title
Body
Generated: Sun Nov 30 09:20:46 2014 | Cross-referenced by PHPXref 0.7.1 |