Correct Aphlict websocket URI construction after PHP8 compatibility changes
[phabricator.git] / src / applications / phortune / provider / PhortuneStripePaymentProvider.php
blob0463881016c6878133658a3c31cf9b352807e698
1 <?php
3 final class PhortuneStripePaymentProvider extends PhortunePaymentProvider {
5 const STRIPE_PUBLISHABLE_KEY = 'stripe.publishable-key';
6 const STRIPE_SECRET_KEY = 'stripe.secret-key';
8 public function isAcceptingLivePayments() {
9 return preg_match('/_live_/', $this->getPublishableKey());
12 public function getName() {
13 return pht('Stripe');
16 public function getConfigureName() {
17 return pht('Add Stripe Payments Account');
20 public function getConfigureDescription() {
21 return pht(
22 'Allows you to accept credit or debit card payments with a '.
23 'stripe.com account.');
26 public function getConfigureProvidesDescription() {
27 return pht('This merchant accepts credit and debit cards via Stripe.');
30 public function getPaymentMethodDescription() {
31 return pht('Add Credit or Debit Card (US and Canada)');
34 public function getPaymentMethodIcon() {
35 return 'Stripe';
38 public function getPaymentMethodProviderDescription() {
39 return pht('Processed by Stripe');
42 public function getDefaultPaymentMethodDisplayName(
43 PhortunePaymentMethod $method) {
44 return pht('Credit/Debit Card');
47 public function getAllConfigurableProperties() {
48 return array(
49 self::STRIPE_PUBLISHABLE_KEY,
50 self::STRIPE_SECRET_KEY,
54 public function getAllConfigurableSecretProperties() {
55 return array(
56 self::STRIPE_SECRET_KEY,
60 public function processEditForm(
61 AphrontRequest $request,
62 array $values) {
64 $errors = array();
65 $issues = array();
67 if (!strlen($values[self::STRIPE_SECRET_KEY])) {
68 $errors[] = pht('Stripe Secret Key is required.');
69 $issues[self::STRIPE_SECRET_KEY] = pht('Required');
72 if (!strlen($values[self::STRIPE_PUBLISHABLE_KEY])) {
73 $errors[] = pht('Stripe Publishable Key is required.');
74 $issues[self::STRIPE_PUBLISHABLE_KEY] = pht('Required');
77 return array($errors, $issues, $values);
80 public function extendEditForm(
81 AphrontRequest $request,
82 AphrontFormView $form,
83 array $values,
84 array $issues) {
86 $form
87 ->appendChild(
88 id(new AphrontFormTextControl())
89 ->setName(self::STRIPE_SECRET_KEY)
90 ->setValue($values[self::STRIPE_SECRET_KEY])
91 ->setError(idx($issues, self::STRIPE_SECRET_KEY, true))
92 ->setLabel(pht('Stripe Secret Key')))
93 ->appendChild(
94 id(new AphrontFormTextControl())
95 ->setName(self::STRIPE_PUBLISHABLE_KEY)
96 ->setValue($values[self::STRIPE_PUBLISHABLE_KEY])
97 ->setError(idx($issues, self::STRIPE_PUBLISHABLE_KEY, true))
98 ->setLabel(pht('Stripe Publishable Key')));
101 public function getConfigureInstructions() {
102 return pht(
103 "To configure Stripe, register or log in to an existing account on ".
104 "[[https://stripe.com | stripe.com]]. Once logged in:\n\n".
105 " - Go to {nav icon=user, name=Your Account > Account Settings ".
106 "> API Keys}\n".
107 " - Copy the **Secret Key** and **Publishable Key** into the fields ".
108 "above.\n\n".
109 "You can either use the test keys to add this provider in test mode, ".
110 "or the live keys to accept live payments.");
113 public function canRunConfigurationTest() {
114 return true;
117 public function runConfigurationTest() {
118 $this->loadStripeAPILibraries();
120 $secret_key = $this->getSecretKey();
121 $account = Stripe_Account::retrieve($secret_key);
125 * @phutil-external-symbol class Stripe_Charge
126 * @phutil-external-symbol class Stripe_CardError
127 * @phutil-external-symbol class Stripe_Account
129 protected function executeCharge(
130 PhortunePaymentMethod $method,
131 PhortuneCharge $charge) {
132 $this->loadStripeAPILibraries();
134 $price = $charge->getAmountAsCurrency();
136 $secret_key = $this->getSecretKey();
137 $params = array(
138 'amount' => $price->getValueInUSDCents(),
139 'currency' => $price->getCurrency(),
140 'customer' => $method->getMetadataValue('stripe.customerID'),
141 'description' => $charge->getPHID(),
142 'capture' => true,
145 $stripe_charge = Stripe_Charge::create($params, $secret_key);
147 $id = $stripe_charge->id;
148 if (!$id) {
149 throw new Exception(pht('Stripe charge call did not return an ID!'));
152 $charge->setMetadataValue('stripe.chargeID', $id);
153 $charge->save();
156 protected function executeRefund(
157 PhortuneCharge $charge,
158 PhortuneCharge $refund) {
159 $this->loadStripeAPILibraries();
161 $charge_id = $charge->getMetadataValue('stripe.chargeID');
162 if (!$charge_id) {
163 throw new Exception(
164 pht('Unable to refund charge; no Stripe chargeID!'));
167 $refund_cents = $refund
168 ->getAmountAsCurrency()
169 ->negate()
170 ->getValueInUSDCents();
172 $secret_key = $this->getSecretKey();
173 $params = array(
174 'amount' => $refund_cents,
177 $stripe_charge = Stripe_Charge::retrieve($charge_id, $secret_key);
178 $stripe_refund = $stripe_charge->refunds->create($params);
180 $id = $stripe_refund->id;
181 if (!$id) {
182 throw new Exception(pht('Stripe refund call did not return an ID!'));
185 $charge->setMetadataValue('stripe.refundID', $id);
186 $charge->save();
189 public function updateCharge(PhortuneCharge $charge) {
190 $this->loadStripeAPILibraries();
192 $charge_id = $charge->getMetadataValue('stripe.chargeID');
193 if (!$charge_id) {
194 throw new Exception(
195 pht('Unable to update charge; no Stripe chargeID!'));
198 $secret_key = $this->getSecretKey();
199 $stripe_charge = Stripe_Charge::retrieve($charge_id, $secret_key);
201 // TODO: Deal with disputes / chargebacks / surprising refunds.
205 private function getPublishableKey() {
206 return $this
207 ->getProviderConfig()
208 ->getMetadataValue(self::STRIPE_PUBLISHABLE_KEY);
211 private function getSecretKey() {
212 return $this
213 ->getProviderConfig()
214 ->getMetadataValue(self::STRIPE_SECRET_KEY);
218 /* -( Adding Payment Methods )--------------------------------------------- */
221 public function canCreatePaymentMethods() {
222 return true;
227 * @phutil-external-symbol class Stripe_Token
228 * @phutil-external-symbol class Stripe_Customer
230 public function createPaymentMethodFromRequest(
231 AphrontRequest $request,
232 PhortunePaymentMethod $method,
233 array $token) {
234 $this->loadStripeAPILibraries();
236 $secret_key = $this->getSecretKey();
237 $stripe_token = $token['stripeCardToken'];
239 // First, make sure the token is valid.
240 $info = id(new Stripe_Token())->retrieve($stripe_token, $secret_key);
242 $account_phid = $method->getAccountPHID();
243 $author_phid = $method->getAuthorPHID();
245 $params = array(
246 'card' => $stripe_token,
247 'description' => $account_phid.':'.$author_phid,
250 // Then, we need to create a Customer in order to be able to charge
251 // the card more than once. We create one Customer for each card;
252 // they do not map to PhortuneAccounts because we allow an account to
253 // have more than one active card.
254 try {
255 $customer = Stripe_Customer::create($params, $secret_key);
256 } catch (Stripe_CardError $ex) {
257 $display_exception = $this->newDisplayExceptionFromCardError($ex);
258 if ($display_exception) {
259 throw $display_exception;
261 throw $ex;
264 $card = $info->card;
266 $method
267 ->setBrand($card->brand)
268 ->setLastFourDigits($card->last4)
269 ->setExpires($card->exp_year, $card->exp_month)
270 ->setMetadata(
271 array(
272 'type' => 'stripe.customer',
273 'stripe.customerID' => $customer->id,
274 'stripe.cardToken' => $stripe_token,
278 public function renderCreatePaymentMethodForm(
279 AphrontRequest $request,
280 array $errors) {
282 $src = 'https://js.stripe.com/v2/';
284 $ccform = id(new PhortuneCreditCardForm())
285 ->setSecurityAssurance(
286 pht('Payments are processed securely by Stripe.'))
287 ->setUser($request->getUser())
288 ->setErrors($errors)
289 ->addScript($src);
291 CelerityAPI::getStaticResourceResponse()
292 ->addContentSecurityPolicyURI('script-src', $src)
293 ->addContentSecurityPolicyURI('frame-src', $src);
295 Javelin::initBehavior(
296 'stripe-payment-form',
297 array(
298 'stripePublishableKey' => $this->getPublishableKey(),
299 'formID' => $ccform->getFormID(),
302 return $ccform->buildForm();
305 private function getStripeShortErrorCode($error_code) {
306 $prefix = 'cc:stripe:';
307 if (strncmp($error_code, $prefix, strlen($prefix))) {
308 return null;
310 return substr($error_code, strlen($prefix));
313 public function validateCreatePaymentMethodToken(array $token) {
314 return isset($token['stripeCardToken']);
317 public function translateCreatePaymentMethodErrorCode($error_code) {
318 $short_code = $this->getStripeShortErrorCode($error_code);
320 if ($short_code) {
321 static $map = array(
322 'error:invalid_number' => PhortuneErrCode::ERR_CC_INVALID_NUMBER,
323 'error:invalid_cvc' => PhortuneErrCode::ERR_CC_INVALID_CVC,
324 'error:invalid_expiry_month' => PhortuneErrCode::ERR_CC_INVALID_EXPIRY,
325 'error:invalid_expiry_year' => PhortuneErrCode::ERR_CC_INVALID_EXPIRY,
328 if (isset($map[$short_code])) {
329 return $map[$short_code];
333 return $error_code;
337 * See https://stripe.com/docs/api#errors for more information on possible
338 * errors.
340 public function getCreatePaymentMethodErrorMessage($error_code) {
341 $short_code = $this->getStripeShortErrorCode($error_code);
342 if (!$short_code) {
343 return null;
346 switch ($short_code) {
347 case 'error:incorrect_number':
348 $error_key = 'number';
349 $message = pht('Invalid or incorrect credit card number.');
350 break;
351 case 'error:incorrect_cvc':
352 $error_key = 'cvc';
353 $message = pht('Card CVC is invalid or incorrect.');
354 break;
355 $error_key = 'exp';
356 $message = pht('Card expiration date is invalid or incorrect.');
357 break;
358 case 'error:invalid_expiry_month':
359 case 'error:invalid_expiry_year':
360 case 'error:invalid_cvc':
361 case 'error:invalid_number':
362 // NOTE: These should be translated into Phortune error codes earlier,
363 // so we don't expect to receive them here. They are listed for clarity
364 // and completeness. If we encounter one, we treat it as an unknown
365 // error.
366 break;
367 case 'error:invalid_amount':
368 case 'error:missing':
369 case 'error:card_declined':
370 case 'error:expired_card':
371 case 'error:duplicate_transaction':
372 case 'error:processing_error':
373 default:
374 // NOTE: These errors currently don't receive a detailed message.
375 // NOTE: We can also end up here with "http:nnn" messages.
377 // TODO: At least some of these should have a better message, or be
378 // translated into common errors above.
379 break;
382 return null;
385 private function loadStripeAPILibraries() {
386 $root = dirname(phutil_get_library_root('phabricator'));
387 require_once $root.'/externals/stripe-php/lib/Stripe.php';
391 private function newDisplayExceptionFromCardError(Stripe_CardError $ex) {
392 $body = $ex->getJSONBody();
393 if (!$body) {
394 return null;
397 $map = idx($body, 'error');
398 if (!$map) {
399 return null;
402 $view = array();
404 $message = idx($map, 'message');
406 $view[] = id(new PHUIInfoView())
407 ->setErrors(array($message));
409 $view[] = phutil_tag(
410 'div',
411 array(
412 'class' => 'mlt mlb',
414 pht('Additional details about this error:'));
416 $rows = array();
418 $rows[] = array(
419 pht('Error Code'),
420 idx($map, 'code'),
423 $rows[] = array(
424 pht('Error Type'),
425 idx($map, 'type'),
428 $param = idx($map, 'param');
429 if (strlen($param)) {
430 $rows[] = array(
431 pht('Error Param'),
432 $param,
436 $decline_code = idx($map, 'decline_code');
437 if (strlen($decline_code)) {
438 $rows[] = array(
439 pht('Decline Code'),
440 $decline_code,
444 $doc_url = idx($map, 'doc_url');
445 if ($doc_url) {
446 $rows[] = array(
447 pht('Learn More'),
448 phutil_tag(
449 'a',
450 array(
451 'href' => $doc_url,
452 'target' => '_blank',
454 $doc_url),
458 $view[] = id(new AphrontTableView($rows))
459 ->setColumnClasses(
460 array(
461 'header',
462 'wide',
465 return id(new PhortuneDisplayException(get_class($ex)))
466 ->setView($view);