[ Index ]

PHP Cross Reference of Phabricator

title

Body

[close]

/src/applications/phortune/provider/ -> PhortunePayPalPaymentProvider.php (source)

   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  }


Generated: Sun Nov 30 09:20:46 2014 Cross-referenced by PHPXref 0.7.1