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() {
16 public function getConfigureName() {
17 return pht('Add Stripe Payments Account');
20 public function getConfigureDescription() {
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() {
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() {
49 self
::STRIPE_PUBLISHABLE_KEY
,
50 self
::STRIPE_SECRET_KEY
,
54 public function getAllConfigurableSecretProperties() {
56 self
::STRIPE_SECRET_KEY
,
60 public function processEditForm(
61 AphrontRequest
$request,
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,
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')))
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() {
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 ".
107 " - Copy the **Secret Key** and **Publishable Key** into the fields ".
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() {
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();
138 'amount' => $price->getValueInUSDCents(),
139 'currency' => $price->getCurrency(),
140 'customer' => $method->getMetadataValue('stripe.customerID'),
141 'description' => $charge->getPHID(),
145 $stripe_charge = Stripe_Charge
::create($params, $secret_key);
147 $id = $stripe_charge->id
;
149 throw new Exception(pht('Stripe charge call did not return an ID!'));
152 $charge->setMetadataValue('stripe.chargeID', $id);
156 protected function executeRefund(
157 PhortuneCharge
$charge,
158 PhortuneCharge
$refund) {
159 $this->loadStripeAPILibraries();
161 $charge_id = $charge->getMetadataValue('stripe.chargeID');
164 pht('Unable to refund charge; no Stripe chargeID!'));
167 $refund_cents = $refund
168 ->getAmountAsCurrency()
170 ->getValueInUSDCents();
172 $secret_key = $this->getSecretKey();
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
;
182 throw new Exception(pht('Stripe refund call did not return an ID!'));
185 $charge->setMetadataValue('stripe.refundID', $id);
189 public function updateCharge(PhortuneCharge
$charge) {
190 $this->loadStripeAPILibraries();
192 $charge_id = $charge->getMetadataValue('stripe.chargeID');
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() {
207 ->getProviderConfig()
208 ->getMetadataValue(self
::STRIPE_PUBLISHABLE_KEY
);
211 private function getSecretKey() {
213 ->getProviderConfig()
214 ->getMetadataValue(self
::STRIPE_SECRET_KEY
);
218 /* -( Adding Payment Methods )--------------------------------------------- */
221 public function canCreatePaymentMethods() {
227 * @phutil-external-symbol class Stripe_Token
228 * @phutil-external-symbol class Stripe_Customer
230 public function createPaymentMethodFromRequest(
231 AphrontRequest
$request,
232 PhortunePaymentMethod
$method,
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();
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.
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;
267 ->setBrand($card->brand
)
268 ->setLastFourDigits($card->last4
)
269 ->setExpires($card->exp_year
, $card->exp_month
)
272 'type' => 'stripe.customer',
273 'stripe.customerID' => $customer->id
,
274 'stripe.cardToken' => $stripe_token,
278 public function renderCreatePaymentMethodForm(
279 AphrontRequest
$request,
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())
291 CelerityAPI
::getStaticResourceResponse()
292 ->addContentSecurityPolicyURI('script-src', $src)
293 ->addContentSecurityPolicyURI('frame-src', $src);
295 Javelin
::initBehavior(
296 'stripe-payment-form',
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))) {
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);
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];
337 * See https://stripe.com/docs/api#errors for more information on possible
340 public function getCreatePaymentMethodErrorMessage($error_code) {
341 $short_code = $this->getStripeShortErrorCode($error_code);
346 switch ($short_code) {
347 case 'error:incorrect_number':
348 $error_key = 'number';
349 $message = pht('Invalid or incorrect credit card number.');
351 case 'error:incorrect_cvc':
353 $message = pht('Card CVC is invalid or incorrect.');
356 $message = pht('Card expiration date is invalid or incorrect.');
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
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':
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.
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();
397 $map = idx($body, 'error');
404 $message = idx($map, 'message');
406 $view[] = id(new PHUIInfoView())
407 ->setErrors(array($message));
409 $view[] = phutil_tag(
412 'class' => 'mlt mlb',
414 pht('Additional details about this error:'));
428 $param = idx($map, 'param');
429 if (strlen($param)) {
436 $decline_code = idx($map, 'decline_code');
437 if (strlen($decline_code)) {
444 $doc_url = idx($map, 'doc_url');
452 'target' => '_blank',
458 $view[] = id(new AphrontTableView($rows))
465 return id(new PhortuneDisplayException(get_class($ex)))