[ Index ]

PHP Cross Reference of Phabricator

title

Body

[close]

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

   1  <?php
   2  
   3  final class PhortuneStripePaymentProvider extends PhortunePaymentProvider {
   4  
   5    const STRIPE_PUBLISHABLE_KEY  = 'stripe.publishable-key';
   6    const STRIPE_SECRET_KEY       = 'stripe.secret-key';
   7  
   8    public function isAcceptingLivePayments() {
   9      return preg_match('/_live_/', $this->getPublishableKey());
  10    }
  11  
  12    public function getName() {
  13      return pht('Stripe');
  14    }
  15  
  16    public function getConfigureName() {
  17      return pht('Add Stripe Payments Account');
  18    }
  19  
  20    public function getConfigureDescription() {
  21      return pht(
  22        'Allows you to accept credit or debit card payments with a '.
  23        'stripe.com account.');
  24    }
  25  
  26    public function getConfigureProvidesDescription() {
  27      return pht(
  28        'This merchant accepts credit and debit cards via Stripe.');
  29    }
  30  
  31    public function getPaymentMethodDescription() {
  32      return pht('Add Credit or Debit Card (US and Canada)');
  33    }
  34  
  35    public function getPaymentMethodIcon() {
  36      return 'Stripe';
  37    }
  38  
  39    public function getPaymentMethodProviderDescription() {
  40      return pht('Processed by Stripe');
  41    }
  42  
  43    public function getDefaultPaymentMethodDisplayName(
  44      PhortunePaymentMethod $method) {
  45      return pht('Credit/Debit Card');
  46    }
  47  
  48    public function getAllConfigurableProperties() {
  49      return array(
  50        self::STRIPE_PUBLISHABLE_KEY,
  51        self::STRIPE_SECRET_KEY,
  52      );
  53    }
  54  
  55    public function getAllConfigurableSecretProperties() {
  56      return array(
  57        self::STRIPE_SECRET_KEY,
  58      );
  59    }
  60  
  61    public function processEditForm(
  62      AphrontRequest $request,
  63      array $values) {
  64  
  65      $errors = array();
  66      $issues = array();
  67  
  68      if (!strlen($values[self::STRIPE_SECRET_KEY])) {
  69        $errors[] = pht('Stripe Secret Key is required.');
  70        $issues[self::STRIPE_SECRET_KEY] = pht('Required');
  71      }
  72  
  73      if (!strlen($values[self::STRIPE_PUBLISHABLE_KEY])) {
  74        $errors[] = pht('Stripe Publishable Key is required.');
  75        $issues[self::STRIPE_PUBLISHABLE_KEY] = pht('Required');
  76      }
  77  
  78      return array($errors, $issues, $values);
  79    }
  80  
  81    public function extendEditForm(
  82      AphrontRequest $request,
  83      AphrontFormView $form,
  84      array $values,
  85      array $issues) {
  86  
  87      $form
  88        ->appendChild(
  89          id(new AphrontFormTextControl())
  90            ->setName(self::STRIPE_SECRET_KEY)
  91            ->setValue($values[self::STRIPE_SECRET_KEY])
  92            ->setError(idx($issues, self::STRIPE_SECRET_KEY, true))
  93            ->setLabel(pht('Stripe Secret Key')))
  94        ->appendChild(
  95          id(new AphrontFormTextControl())
  96            ->setName(self::STRIPE_PUBLISHABLE_KEY)
  97            ->setValue($values[self::STRIPE_PUBLISHABLE_KEY])
  98            ->setError(idx($issues, self::STRIPE_PUBLISHABLE_KEY, true))
  99            ->setLabel(pht('Stripe Publishable Key')));
 100    }
 101  
 102    public function getConfigureInstructions() {
 103      return pht(
 104        "To configure Stripe, register or log in to an existing account on ".
 105        "[[https://stripe.com | stripe.com]]. Once logged in:\n\n".
 106        "  - Go to {nav icon=user, name=Your Account > Account Settings ".
 107        "> API Keys}\n".
 108        "  - Copy the **Secret Key** and **Publishable Key** into the fields ".
 109        "above.\n\n".
 110        "You can either use the test keys to add this provider in test mode, ".
 111        "or the live keys to accept live payments.");
 112    }
 113  
 114    public function canRunConfigurationTest() {
 115      return true;
 116    }
 117  
 118    public function runConfigurationTest() {
 119      $this->loadStripeAPILibraries();
 120  
 121      $secret_key = $this->getSecretKey();
 122      $account = Stripe_Account::retrieve($secret_key);
 123    }
 124  
 125    /**
 126     * @phutil-external-symbol class Stripe_Charge
 127     * @phutil-external-symbol class Stripe_CardError
 128     * @phutil-external-symbol class Stripe_Account
 129     */
 130    protected function executeCharge(
 131      PhortunePaymentMethod $method,
 132      PhortuneCharge $charge) {
 133      $this->loadStripeAPILibraries();
 134  
 135      $price = $charge->getAmountAsCurrency();
 136  
 137      $secret_key = $this->getSecretKey();
 138      $params = array(
 139        'amount'      => $price->getValueInUSDCents(),
 140        'currency'    => $price->getCurrency(),
 141        'customer'    => $method->getMetadataValue('stripe.customerID'),
 142        'description' => $charge->getPHID(),
 143        'capture'     => true,
 144      );
 145  
 146      $stripe_charge = Stripe_Charge::create($params, $secret_key);
 147  
 148      $id = $stripe_charge->id;
 149      if (!$id) {
 150        throw new Exception('Stripe charge call did not return an ID!');
 151      }
 152  
 153      $charge->setMetadataValue('stripe.chargeID', $id);
 154      $charge->save();
 155    }
 156  
 157    protected function executeRefund(
 158      PhortuneCharge $charge,
 159      PhortuneCharge $refund) {
 160      $this->loadStripeAPILibraries();
 161  
 162      $charge_id = $charge->getMetadataValue('stripe.chargeID');
 163      if (!$charge_id) {
 164        throw new Exception(
 165          pht('Unable to refund charge; no Stripe chargeID!'));
 166      }
 167  
 168      $refund_cents = $refund
 169        ->getAmountAsCurrency()
 170        ->negate()
 171        ->getValueInUSDCents();
 172  
 173      $secret_key = $this->getSecretKey();
 174      $params = array(
 175        'amount' => $refund_cents,
 176      );
 177  
 178      $stripe_charge = Stripe_Charge::retrieve($charge_id, $secret_key);
 179      $stripe_refund = $stripe_charge->refunds->create($params);
 180  
 181      $id = $stripe_refund->id;
 182      if (!$id) {
 183        throw new Exception(pht('Stripe refund call did not return an ID!'));
 184      }
 185  
 186      $charge->setMetadataValue('stripe.refundID', $id);
 187      $charge->save();
 188    }
 189  
 190    public function updateCharge(PhortuneCharge $charge) {
 191      $this->loadStripeAPILibraries();
 192  
 193      $charge_id = $charge->getMetadataValue('stripe.chargeID');
 194      if (!$charge_id) {
 195        throw new Exception(
 196          pht('Unable to update charge; no Stripe chargeID!'));
 197      }
 198  
 199      $secret_key = $this->getSecretKey();
 200      $stripe_charge = Stripe_Charge::retrieve($charge_id, $secret_key);
 201  
 202      // TODO: Deal with disputes / chargebacks / surprising refunds.
 203  
 204    }
 205  
 206    private function getPublishableKey() {
 207      return $this
 208        ->getProviderConfig()
 209        ->getMetadataValue(self::STRIPE_PUBLISHABLE_KEY);
 210    }
 211  
 212    private function getSecretKey() {
 213      return $this
 214        ->getProviderConfig()
 215        ->getMetadataValue(self::STRIPE_SECRET_KEY);
 216    }
 217  
 218  
 219  /* -(  Adding Payment Methods  )--------------------------------------------- */
 220  
 221  
 222    public function canCreatePaymentMethods() {
 223      return true;
 224    }
 225  
 226  
 227    /**
 228     * @phutil-external-symbol class Stripe_Token
 229     * @phutil-external-symbol class Stripe_Customer
 230     */
 231    public function createPaymentMethodFromRequest(
 232      AphrontRequest $request,
 233      PhortunePaymentMethod $method,
 234      array $token) {
 235      $this->loadStripeAPILibraries();
 236  
 237      $errors = array();
 238  
 239      $secret_key = $this->getSecretKey();
 240      $stripe_token = $token['stripeCardToken'];
 241  
 242      // First, make sure the token is valid.
 243      $info = id(new Stripe_Token())->retrieve($stripe_token, $secret_key);
 244  
 245      $account_phid = $method->getAccountPHID();
 246      $author_phid = $method->getAuthorPHID();
 247  
 248      $params = array(
 249        'card' => $stripe_token,
 250        'description' => $account_phid.':'.$author_phid,
 251      );
 252  
 253      // Then, we need to create a Customer in order to be able to charge
 254      // the card more than once. We create one Customer for each card;
 255      // they do not map to PhortuneAccounts because we allow an account to
 256      // have more than one active card.
 257      $customer = Stripe_Customer::create($params, $secret_key);
 258  
 259      $card = $info->card;
 260  
 261      $method
 262        ->setBrand($card->brand)
 263        ->setLastFourDigits($card->last4)
 264        ->setExpires($card->exp_year, $card->exp_month)
 265        ->setMetadata(
 266          array(
 267            'type' => 'stripe.customer',
 268            'stripe.customerID' => $customer->id,
 269            'stripe.cardToken' => $stripe_token,
 270          ));
 271  
 272      return $errors;
 273    }
 274  
 275    public function renderCreatePaymentMethodForm(
 276      AphrontRequest $request,
 277      array $errors) {
 278  
 279      $ccform = id(new PhortuneCreditCardForm())
 280        ->setUser($request->getUser())
 281        ->setErrors($errors)
 282        ->addScript('https://js.stripe.com/v2/');
 283  
 284      Javelin::initBehavior(
 285        'stripe-payment-form',
 286        array(
 287          'stripePublishableKey' => $this->getPublishableKey(),
 288          'formID'               => $ccform->getFormID(),
 289        ));
 290  
 291      return $ccform->buildForm();
 292    }
 293  
 294    private function getStripeShortErrorCode($error_code) {
 295      $prefix = 'cc:stripe:';
 296      if (strncmp($error_code, $prefix, strlen($prefix))) {
 297        return null;
 298      }
 299      return substr($error_code, strlen($prefix));
 300    }
 301  
 302    public function validateCreatePaymentMethodToken(array $token) {
 303      return isset($token['stripeCardToken']);
 304    }
 305  
 306    public function translateCreatePaymentMethodErrorCode($error_code) {
 307      $short_code = $this->getStripeShortErrorCode($error_code);
 308  
 309      if ($short_code) {
 310        static $map = array(
 311          'error:invalid_number'        => PhortuneErrCode::ERR_CC_INVALID_NUMBER,
 312          'error:invalid_cvc'           => PhortuneErrCode::ERR_CC_INVALID_CVC,
 313          'error:invalid_expiry_month'  => PhortuneErrCode::ERR_CC_INVALID_EXPIRY,
 314          'error:invalid_expiry_year'   => PhortuneErrCode::ERR_CC_INVALID_EXPIRY,
 315        );
 316  
 317        if (isset($map[$short_code])) {
 318          return $map[$short_code];
 319        }
 320      }
 321  
 322      return $error_code;
 323    }
 324  
 325    /**
 326     * See https://stripe.com/docs/api#errors for more information on possible
 327     * errors.
 328     */
 329    public function getCreatePaymentMethodErrorMessage($error_code) {
 330      $short_code = $this->getStripeShortErrorCode($error_code);
 331      if (!$short_code) {
 332        return null;
 333      }
 334  
 335      switch ($short_code) {
 336        case 'error:incorrect_number':
 337          $error_key = 'number';
 338          $message = pht('Invalid or incorrect credit card number.');
 339          break;
 340        case 'error:incorrect_cvc':
 341          $error_key = 'cvc';
 342          $message = pht('Card CVC is invalid or incorrect.');
 343          break;
 344          $error_key = 'exp';
 345          $message = pht('Card expiration date is invalid or incorrect.');
 346          break;
 347        case 'error:invalid_expiry_month':
 348        case 'error:invalid_expiry_year':
 349        case 'error:invalid_cvc':
 350        case 'error:invalid_number':
 351          // NOTE: These should be translated into Phortune error codes earlier,
 352          // so we don't expect to receive them here. They are listed for clarity
 353          // and completeness. If we encounter one, we treat it as an unknown
 354          // error.
 355          break;
 356        case 'error:invalid_amount':
 357        case 'error:missing':
 358        case 'error:card_declined':
 359        case 'error:expired_card':
 360        case 'error:duplicate_transaction':
 361        case 'error:processing_error':
 362        default:
 363          // NOTE: These errors currently don't recevive a detailed message.
 364          // NOTE: We can also end up here with "http:nnn" messages.
 365  
 366          // TODO: At least some of these should have a better message, or be
 367          // translated into common errors above.
 368          break;
 369      }
 370  
 371      return null;
 372    }
 373  
 374    private function loadStripeAPILibraries() {
 375      $root = dirname(phutil_get_library_root('phabricator'));
 376      require_once $root.'/externals/stripe-php/lib/Stripe.php';
 377    }
 378  
 379  }


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