[ Index ] |
PHP Cross Reference of Phabricator |
[Summary view] [Print] [Text view]
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 }
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 |