@recaptime-dev's working patches + fork for Phorge, a community fork of Phabricator. (Upstream dev and stable branches are at upstream/main and upstream/stable respectively.) hq.recaptime.dev/wiki/Phorge
phorge phabricator
1
fork

Configure Feed

Select the types of activity you want to include in your feed.

General cleanup for adding payment methods in Phortune

Summary:
This has no real behavioral changes (except better error handling), it just factors things out to be a bit cleaner. In particular:

- Move more shared form behaviors into the common JS form component.
- Move more error handling into shared pathways.
- Make the specialized Stripe / Balanced methods do less work.

This needs some more polish for nontrival errors (especially on the Balanced side) but none of the error behavior is worse than it was and a lot of it is much better.

Ref T2787.

Test Plan: Hit all invalid form errors, added valid payment methods with Stripe and Balacned.

Reviewers: btrahan, chad

Reviewed By: btrahan

CC: aran

Maniphest Tasks: T2787

Differential Revision: https://secure.phabricator.com/D5771

+435 -279
+19
resources/sql/patches/20130423.phortunepaymentrevised.sql
··· 1 + TRUNCATE TABLE {$NAMESPACE}_phortune.phortune_paymentmethod; 2 + 3 + ALTER TABLE {$NAMESPACE}_phortune.phortune_paymentmethod 4 + ADD brand VARCHAR(64) NOT NULL; 5 + 6 + ALTER TABLE {$NAMESPACE}_phortune.phortune_paymentmethod 7 + ADD expires VARCHAR(16) NOT NULL; 8 + 9 + ALTER TABLE {$NAMESPACE}_phortune.phortune_paymentmethod 10 + ADD providerType VARCHAR(16) NOT NULL; 11 + 12 + ALTER TABLE {$NAMESPACE}_phortune.phortune_paymentmethod 13 + ADD providerDomain VARCHAR(64) NOT NULL; 14 + 15 + ALTER TABLE {$NAMESPACE}_phortune.phortune_paymentmethod 16 + ADD lastFourDigits VARCHAR(16) NOT NULL; 17 + 18 + ALTER TABLE {$NAMESPACE}_phortune.phortune_paymentmethod 19 + DROP expiresEpoch;
+8 -9
src/__celerity_resource_map__.php
··· 1276 1276 ), 1277 1277 'javelin-behavior-balanced-payment-form' => 1278 1278 array( 1279 - 'uri' => '/res/2a850a31/rsrc/js/application/phortune/behavior-balanced-payment-form.js', 1279 + 'uri' => '/res/6876492d/rsrc/js/application/phortune/behavior-balanced-payment-form.js', 1280 1280 'type' => 'js', 1281 1281 'requires' => 1282 1282 array( 1283 1283 0 => 'javelin-behavior', 1284 1284 1 => 'javelin-dom', 1285 - 2 => 'javelin-json', 1286 - 3 => 'javelin-workflow', 1287 - 4 => 'phortune-credit-card-form', 1285 + 2 => 'phortune-credit-card-form', 1288 1286 ), 1289 1287 'disk' => '/rsrc/js/application/phortune/behavior-balanced-payment-form.js', 1290 1288 ), ··· 2272 2270 ), 2273 2271 'javelin-behavior-stripe-payment-form' => 2274 2272 array( 2275 - 'uri' => '/res/2ae12d96/rsrc/js/application/phortune/behavior-stripe-payment-form.js', 2273 + 'uri' => '/res/c1a12d77/rsrc/js/application/phortune/behavior-stripe-payment-form.js', 2276 2274 'type' => 'js', 2277 2275 'requires' => 2278 2276 array( 2279 2277 0 => 'javelin-behavior', 2280 2278 1 => 'javelin-dom', 2281 - 2 => 'javelin-json', 2282 - 3 => 'javelin-workflow', 2283 - 4 => 'phortune-credit-card-form', 2279 + 2 => 'phortune-credit-card-form', 2284 2280 ), 2285 2281 'disk' => '/rsrc/js/application/phortune/behavior-stripe-payment-form.js', 2286 2282 ), ··· 3605 3601 ), 3606 3602 'phortune-credit-card-form' => 3607 3603 array( 3608 - 'uri' => '/res/7be5799a/rsrc/js/application/phortune/phortune-credit-card-form.js', 3604 + 'uri' => '/res/bc948778/rsrc/js/application/phortune/phortune-credit-card-form.js', 3609 3605 'type' => 'js', 3610 3606 'requires' => 3611 3607 array( 3612 3608 0 => 'javelin-install', 3613 3609 1 => 'javelin-dom', 3610 + 2 => 'javelin-json', 3611 + 3 => 'javelin-workflow', 3612 + 4 => 'javelin-util', 3614 3613 ), 3615 3614 'disk' => '/rsrc/js/application/phortune/phortune-credit-card-form.js', 3616 3615 ),
+3
src/__phutil_library_map__.php
··· 1583 1583 'PhortuneBalancedPaymentProvider' => 'applications/phortune/provider/PhortuneBalancedPaymentProvider.php', 1584 1584 'PhortuneCart' => 'applications/phortune/storage/PhortuneCart.php', 1585 1585 'PhortuneCharge' => 'applications/phortune/storage/PhortuneCharge.php', 1586 + 'PhortuneConstants' => 'applications/phortune/constants/PhortuneConstants.php', 1586 1587 'PhortuneController' => 'applications/phortune/controller/PhortuneController.php', 1587 1588 'PhortuneCreditCardForm' => 'applications/phortune/view/PhortuneCreditCardForm.php', 1588 1589 'PhortuneDAO' => 'applications/phortune/storage/PhortuneDAO.php', 1590 + 'PhortuneErrCode' => 'applications/phortune/constants/PhortuneErrCode.php', 1589 1591 'PhortuneLandingController' => 'applications/phortune/controller/PhortuneLandingController.php', 1590 1592 'PhortuneMonthYearExpiryControl' => 'applications/phortune/control/PhortuneMonthYearExpiryControl.php', 1591 1593 'PhortuneMultiplePaymentProvidersException' => 'applications/phortune/exception/PhortuneMultiplePaymentProvidersException.php', ··· 3308 3310 'PhortuneCharge' => 'PhortuneDAO', 3309 3311 'PhortuneController' => 'PhabricatorController', 3310 3312 'PhortuneDAO' => 'PhabricatorLiskDAO', 3313 + 'PhortuneErrCode' => 'PhortuneConstants', 3311 3314 'PhortuneLandingController' => 'PhortuneController', 3312 3315 'PhortuneMonthYearExpiryControl' => 'AphrontFormControl', 3313 3316 'PhortuneMultiplePaymentProvidersException' => 'Exception',
+5
src/applications/phortune/constants/PhortuneConstants.php
··· 1 + <?php 2 + 3 + abstract class PhortuneConstants { 4 + 5 + }
+11
src/applications/phortune/constants/PhortuneErrCode.php
··· 1 + <?php 2 + 3 + final class PhortuneErrCode extends PhortuneConstants { 4 + 5 + // NOTE: These constants also appear in Javascript. 6 + 7 + const ERR_CC_INVALID_NUMBER = 'cc:invalid:number'; 8 + const ERR_CC_INVALID_CVC = 'cc:invalid:cvc'; 9 + const ERR_CC_INVALID_EXPIRY = 'cc:invalid:expiry'; 10 + 11 + }
+1 -5
src/applications/phortune/controller/PhortuneAccountViewController.php
··· 111 111 112 112 foreach ($methods as $method) { 113 113 $item = new PhabricatorObjectItemView(); 114 - $item->setHeader($method->getName()); 114 + $item->setHeader($method->getBrand().' / '.$method->getLastFourDigits()); 115 115 116 116 switch ($method->getStatus()) { 117 117 case PhortunePaymentMethod::STATUS_ACTIVE: ··· 125 125 'Added %s by %s', 126 126 phabricator_datetime($method->getDateCreated(), $user), 127 127 $this->getHandle($method->getAuthorPHID())->renderLink())); 128 - 129 - if ($method->getExpiresEpoch() < time() + (60 * 60 * 24 * 30)) { 130 - $item->addAttribute(pht('Expires Soon!')); 131 - } 132 128 133 129 $list->addItem($item); 134 130 }
+88 -2
src/applications/phortune/controller/PhortunePaymentMethodEditController.php
··· 48 48 ->setAccountPHID($account->getPHID()) 49 49 ->setAuthorPHID($user->getPHID()) 50 50 ->setStatus(PhortunePaymentMethod::STATUS_ACTIVE) 51 - ->setMetadataValue('providerKey', $provider->getProviderKey()); 51 + ->setProviderType($provider->getProviderType()) 52 + ->setProviderDomain($provider->getProviderDomain()); 53 + 54 + if (!$errors) { 55 + $errors = $this->processClientErrors( 56 + $provider, 57 + $request->getStr('errors')); 58 + } 52 59 53 - $errors = $provider->createPaymentMethodFromRequest($request, $method); 60 + if (!$errors) { 61 + $client_token_raw = $request->getStr('token'); 62 + $client_token = json_decode($client_token_raw, true); 63 + if (!is_array($client_token)) { 64 + $errors[] = pht( 65 + 'There was an error decoding token information submitted by the '. 66 + 'client. Expected a JSON-encoded token dictionary, received: %s.', 67 + nonempty($client_token_raw, pht('nothing'))); 68 + } else { 69 + if (!$provider->validateCreatePaymentMethodToken($client_token)) { 70 + $errors[] = pht( 71 + 'There was an error with the payment token submitted by the '. 72 + 'client. Expected a valid dictionary, received: %s.', 73 + $client_token_raw); 74 + } 75 + } 76 + if (!$errors) { 77 + $errors = $provider->createPaymentMethodFromRequest( 78 + $request, 79 + $method, 80 + $client_token); 81 + } 82 + } 54 83 55 84 if (!$errors) { 56 85 $method->save(); ··· 150 179 'device' => true, 151 180 'dust' => true, 152 181 )); 182 + } 183 + 184 + private function processClientErrors( 185 + PhortunePaymentProvider $provider, 186 + $client_errors_raw) { 187 + 188 + $errors = array(); 189 + 190 + $client_errors = json_decode($client_errors_raw, true); 191 + if (!is_array($client_errors)) { 192 + $errors[] = pht( 193 + 'There was an error decoding error information submitted by the '. 194 + 'client. Expected a JSON-encoded list of error codes, received: %s.', 195 + nonempty($client_errors_raw, pht('nothing'))); 196 + } 197 + 198 + foreach (array_unique($client_errors) as $key => $client_error) { 199 + $client_errors[$key] = $provider->translateCreatePaymentMethodErrorCode( 200 + $client_error); 201 + } 202 + 203 + foreach (array_unique($client_errors) as $client_error) { 204 + switch ($client_error) { 205 + case PhortuneErrCode::ERR_CC_INVALID_NUMBER: 206 + $message = pht( 207 + 'The card number you entered is not a valid card number. Check '. 208 + 'that you entered it correctly.'); 209 + break; 210 + case PhortuneErrCode::ERR_CC_INVALID_CVC: 211 + $message = pht( 212 + 'The CVC code you entered is not a valid CVC code. Check that '. 213 + 'you entered it correctly. The CVC code is a 3-digit or 4-digit '. 214 + 'numeric code which usually appears on the back of the card.'); 215 + break; 216 + case PhortuneErrCode::ERR_CC_INVALID_EXPIRY: 217 + $message = pht( 218 + 'The card expiration date is not a valid expiration date. Check '. 219 + 'that you entered it correctly. You can not add an expired card '. 220 + 'as a payment method.'); 221 + break; 222 + default: 223 + $message = $provider->getCreatePaymentErrorMessage($client_error); 224 + if (!$message) { 225 + $message = pht( 226 + "There was an unexpected error ('%s') processing payment ". 227 + "information.", 228 + $client_error); 229 + 230 + phlog($message); 231 + } 232 + break; 233 + } 234 + 235 + $errors[$client_error] = $message; 236 + } 237 + 238 + return $errors; 153 239 } 154 240 155 241 }
+72 -68
src/applications/phortune/provider/PhortuneBalancedPaymentProvider.php
··· 55 55 return true; 56 56 } 57 57 58 + public function validateCreatePaymentMethodToken(array $token) { 59 + return isset($token['balancedMarketplaceURI']); 60 + } 61 + 58 62 59 63 /** 64 + * @phutil-external-symbol class Balanced\Card 60 65 * @phutil-external-symbol class Balanced\Settings 61 66 * @phutil-external-symbol class Balanced\Marketplace 62 67 * @phutil-external-symbol class RESTful\Exceptions\HTTPError 63 68 */ 64 69 public function createPaymentMethodFromRequest( 65 70 AphrontRequest $request, 66 - PhortunePaymentMethod $method) { 67 - 68 - $card_errors = $request->getStr('cardErrors'); 69 - $balanced_data = $request->getStr('balancedCardData'); 71 + PhortunePaymentMethod $method, 72 + array $token) { 70 73 71 74 $errors = array(); 72 - if ($card_errors) { 73 - $raw_errors = json_decode($card_errors); 74 - $errors = $this->parseRawCreatePaymentMethodErrors($raw_errors); 75 - } 76 75 77 - if (!$errors) { 78 - $data = json_decode($balanced_data, true); 79 - if (!is_array($data)) { 80 - $errors[] = pht('An error occurred decoding card data.'); 81 - } 82 - } 83 - 84 - if (!$errors) { 85 - $root = dirname(phutil_get_library_root('phabricator')); 86 - require_once $root.'/externals/httpful/bootstrap.php'; 87 - require_once $root.'/externals/restful/bootstrap.php'; 88 - require_once $root.'/externals/balanced-php/bootstrap.php'; 89 - 90 - $account_phid = $method->getAccountPHID(); 91 - $author_phid = $method->getAuthorPHID(); 92 - $description = $account_phid.':'.$author_phid; 76 + $root = dirname(phutil_get_library_root('phabricator')); 77 + require_once $root.'/externals/httpful/bootstrap.php'; 78 + require_once $root.'/externals/restful/bootstrap.php'; 79 + require_once $root.'/externals/balanced-php/bootstrap.php'; 93 80 94 - try { 81 + $account_phid = $method->getAccountPHID(); 82 + $author_phid = $method->getAuthorPHID(); 83 + $description = $account_phid.':'.$author_phid; 95 84 96 - Balanced\Settings::$api_key = $this->getSecretKey(); 97 - $buyer = Balanced\Marketplace::mine()->createBuyer( 98 - null, 99 - $data['uri'], 100 - array( 101 - 'description' => $description, 102 - )); 85 + try { 86 + Balanced\Settings::$api_key = $this->getSecretKey(); 103 87 104 - } catch (RESTful\Exceptions\HTTPError $error) { 105 - // NOTE: This exception doesn't print anything meaningful if it escapes 106 - // to top level. Replace it with something slightly readable. 107 - throw new Exception($error->response->body->description); 108 - } 88 + $card = Balanced\Card::get($token['balancedMarketplaceURI']); 109 89 110 - $exp_string = $data['expiration_year'].'-'.$data['expiration_month']; 111 - $epoch = strtotime($exp_string); 90 + $buyer = Balanced\Marketplace::mine()->createBuyer( 91 + null, 92 + $card->uri, 93 + array( 94 + 'description' => $description, 95 + )); 112 96 113 - $method 114 - ->setName($data['brand'].' / '.$data['last_four']) 115 - ->setExpiresEpoch($epoch) 116 - ->setMetadata( 117 - array( 118 - 'type' => 'balanced.account', 119 - 'balanced.accountURI' => $buyer->uri, 120 - 'balanced.cardURI' => $data['uri'], 121 - )); 97 + } catch (RESTful\Exceptions\HTTPError $error) { 98 + // NOTE: This exception doesn't print anything meaningful if it escapes 99 + // to top level. Replace it with something slightly readable. 100 + throw new Exception($error->response->body->description); 122 101 } 123 102 103 + $method 104 + ->setBrand($card->brand) 105 + ->setLastFourDigits($card->last_four) 106 + ->setExpires($card->expiration_year, $card->expiration_month) 107 + ->setMetadata( 108 + array( 109 + 'type' => 'balanced.account', 110 + 'balanced.accountURI' => $buyer->uri, 111 + 'balanced.cardURI' => $card->uri, 112 + )); 113 + 124 114 return $errors; 125 115 } 126 116 ··· 130 120 131 121 $ccform = id(new PhortuneCreditCardForm()) 132 122 ->setUser($request->getUser()) 133 - ->setCardNumberError(isset($errors['number']) ? pht('Invalid') : true) 134 - ->setCardCVCError(isset($errors['cvc']) ? pht('Invalid') : true) 135 - ->setCardExpirationError(isset($errors['exp']) ? pht('Invalid') : null) 123 + ->setErrors($errors) 136 124 ->addScript('https://js.balancedpayments.com/v1/balanced.js'); 137 125 138 126 Javelin::initBehavior( ··· 145 133 return $ccform->buildForm(); 146 134 } 147 135 148 - private function parseRawCreatePaymentMethodErrors(array $raw_errors) { 149 - $errors = array(); 136 + private function getBalancedShortErrorCode($error_code) { 137 + $prefix = 'cc:balanced:'; 138 + if (strncmp($error_code, $prefix, strlen($prefix))) { 139 + return null; 140 + } 141 + return substr($error_code, strlen($prefix)); 142 + } 150 143 151 - foreach ($raw_errors as $error) { 152 - switch ($error) { 153 - case 'number': 154 - $errors[$error] = pht('Card number is incorrect or invalid.'); 155 - break; 156 - case 'cvc': 157 - $errors[$error] = pht('CVC code is incorrect or invalid.'); 158 - break; 159 - case 'exp': 160 - $errors[$error] = pht('Card expiration date is incorrect.'); 161 - break; 162 - default: 163 - $errors[] = $error; 164 - break; 144 + public function translateCreatePaymentMethodErrorCode($error_code) { 145 + $short_code = $this->getBalancedShortErrorCode($error_code); 146 + 147 + if ($short_code) { 148 + static $map = array( 149 + ); 150 + 151 + if (isset($map[$short_code])) { 152 + return $map[$short_code]; 165 153 } 166 154 } 167 155 168 - return $errors; 156 + return $error_code; 157 + } 158 + 159 + public function getCreatePaymentMethodErrorMessage($error_code) { 160 + $short_code = $this->getBalancedShortErrorCode($error_code); 161 + if (!$short_code) { 162 + return null; 163 + } 164 + 165 + switch ($short_code) { 166 + 167 + default: 168 + break; 169 + } 170 + 171 + 172 + return null; 169 173 } 170 174 171 175 }
+26 -1
src/applications/phortune/provider/PhortunePaymentProvider.php
··· 95 95 /** 96 96 * @task addmethod 97 97 */ 98 + public function translateCreatePaymentMethodErrorCode($error_code) { 99 + throw new PhortuneNotImplementedException($this); 100 + } 101 + 102 + 103 + /** 104 + * @task addmethod 105 + */ 106 + public function getCreatePaymentMethodErrorMessage($error_code) { 107 + throw new PhortuneNotImplementedException($this); 108 + } 109 + 110 + 111 + /** 112 + * @task addmethod 113 + */ 114 + public function validateCreatePaymentMethodToken(array $token) { 115 + throw new PhortuneNotImplementedException($this); 116 + } 117 + 118 + 119 + /** 120 + * @task addmethod 121 + */ 98 122 public function createPaymentMethodFromRequest( 99 123 AphrontRequest $request, 100 - PhortunePaymentMethod $method) { 124 + PhortunePaymentMethod $method, 125 + array $token) { 101 126 throw new PhortuneNotImplementedException($this); 102 127 } 103 128
+100 -100
src/applications/phortune/provider/PhortuneStripePaymentProvider.php
··· 81 81 */ 82 82 public function createPaymentMethodFromRequest( 83 83 AphrontRequest $request, 84 - PhortunePaymentMethod $method) { 85 - 86 - $card_errors = $request->getStr('cardErrors'); 87 - $stripe_token = $request->getStr('stripeToken'); 84 + PhortunePaymentMethod $method, 85 + array $token) { 88 86 89 87 $errors = array(); 90 - if ($card_errors) { 91 - $raw_errors = json_decode($card_errors); 92 - $errors = $this->parseRawCreatePaymentMethodErrors($raw_errors); 93 - } 94 88 95 - if (!$errors) { 96 - if (!$stripe_token) { 97 - $errors[] = pht('There was an unknown error processing your card.'); 98 - } 99 - } 89 + $root = dirname(phutil_get_library_root('phabricator')); 90 + require_once $root.'/externals/stripe-php/lib/Stripe.php'; 100 91 101 - if (!$errors) { 102 - $root = dirname(phutil_get_library_root('phabricator')); 103 - require_once $root.'/externals/stripe-php/lib/Stripe.php'; 104 - 105 - try { 106 - // First, make sure the token is valid. 107 - $secret_key = $this->getSecretKey(); 92 + $secret_key = $this->getSecretKey(); 93 + $stripe_token = $token['stripeCardToken']; 108 94 109 - $info = id(new Stripe_Token())->retrieve($stripe_token, $secret_key); 95 + // First, make sure the token is valid. 96 + $info = id(new Stripe_Token())->retrieve($stripe_token, $secret_key); 110 97 111 - $account_phid = $method->getAccountPHID(); 112 - $author_phid = $method->getAuthorPHID(); 98 + $account_phid = $method->getAccountPHID(); 99 + $author_phid = $method->getAuthorPHID(); 113 100 114 - $params = array( 115 - 'card' => $stripe_token, 116 - 'description' => $account_phid.':'.$author_phid, 117 - ); 101 + $params = array( 102 + 'card' => $stripe_token, 103 + 'description' => $account_phid.':'.$author_phid, 104 + ); 118 105 119 - // Then, we need to create a Customer in order to be able to charge 120 - // the card more than once. We create one Customer for each card; 121 - // they do not map to PhortuneAccounts because we allow an account to 122 - // have more than one active card. 123 - $customer = Stripe_Customer::create($params, $secret_key); 106 + // Then, we need to create a Customer in order to be able to charge 107 + // the card more than once. We create one Customer for each card; 108 + // they do not map to PhortuneAccounts because we allow an account to 109 + // have more than one active card. 110 + $customer = Stripe_Customer::create($params, $secret_key); 124 111 125 - $card = $info->card; 126 - $method 127 - ->setName($card->type.' / '.$card->last4) 128 - ->setExpiresEpoch(strtotime($card->exp_year.'-'.$card->exp_month)) 129 - ->setMetadata( 130 - array( 131 - 'type' => 'stripe.customer', 132 - 'stripe.customerID' => $customer->id, 133 - 'stripe.tokenID' => $stripe_token, 134 - )); 135 - } catch (Exception $ex) { 136 - phlog($ex); 137 - $errors[] = pht( 138 - 'There was an error communicating with the payments backend.'); 139 - } 140 - } 112 + $card = $info->card; 113 + $method 114 + ->setBrand($card->type) 115 + ->setLastFourDigits($card->last4) 116 + ->setExpires($card->exp_year, $card->exp_month) 117 + ->setMetadata( 118 + array( 119 + 'type' => 'stripe.customer', 120 + 'stripe.customerID' => $customer->id, 121 + 'stripe.cardToken' => $stripe_token, 122 + )); 141 123 142 124 return $errors; 143 125 } ··· 148 130 149 131 $ccform = id(new PhortuneCreditCardForm()) 150 132 ->setUser($request->getUser()) 151 - ->setCardNumberError(isset($errors['number']) ? pht('Invalid') : true) 152 - ->setCardCVCError(isset($errors['cvc']) ? pht('Invalid') : true) 153 - ->setCardExpirationError(isset($errors['exp']) ? pht('Invalid') : null) 133 + ->setErrors($errors) 154 134 ->addScript('https://js.stripe.com/v2/'); 155 135 156 136 Javelin::initBehavior( ··· 163 143 return $ccform->buildForm(); 164 144 } 165 145 146 + private function getStripeShortErrorCode($error_code) { 147 + $prefix = 'cc:stripe:'; 148 + if (strncmp($error_code, $prefix, strlen($prefix))) { 149 + return null; 150 + } 151 + return substr($error_code, strlen($prefix)); 152 + } 153 + 154 + public function validateCreatePaymentMethodToken(array $token) { 155 + return isset($token['stripeCardToken']); 156 + } 157 + 158 + public function translateCreatePaymentMethodErrorCode($error_code) { 159 + $short_code = $this->getStripeShortErrorCode($error_code); 160 + 161 + if ($short_code) { 162 + static $map = array( 163 + 'error:invalid_number' => PhortuneErrCode::ERR_CC_INVALID_NUMBER, 164 + 'error:invalid_cvc' => PhortuneErrCode::ERR_CC_INVALID_CVC, 165 + 'error:invalid_expiry_month' => PhortuneErrCode::ERR_CC_INVALID_EXPIRY, 166 + 'error:invalid_expiry_year' => PhortuneErrCode::ERR_CC_INVALID_EXPIRY, 167 + ); 168 + 169 + if (isset($map[$short_code])) { 170 + return $map[$short_code]; 171 + } 172 + } 173 + 174 + return $error_code; 175 + } 166 176 167 177 /** 168 - * Stripe JS and calls to Stripe handle all errors with processing this 169 - * form. This function takes the raw errors - in the form of an array 170 - * where each elementt is $type => $message - and figures out what if 171 - * any fields were invalid and pulls the messages into a flat object. 172 - * 173 178 * See https://stripe.com/docs/api#errors for more information on possible 174 179 * errors. 175 180 */ 176 - private function parseRawCreatePaymentMethodErrors(array $raw_errors) { 177 - $errors = array(); 181 + public function getCreatePaymentMethodErrorMessage($error_code) { 182 + $short_code = $this->getStripeShortErrorCode($error_code); 183 + if (!$short_code) { 184 + return null; 185 + } 178 186 179 - foreach ($raw_errors as $type) { 180 - $error_key = null; 181 - $message = pht('A card processing error has occurred.'); 182 - switch ($type) { 183 - case 'number': 184 - case 'invalid_number': 185 - case 'incorrect_number': 186 - $error_key = 'number'; 187 - $message = pht('Invalid or incorrect credit card number.'); 188 - break; 189 - case 'cvc': 190 - case 'invalid_cvc': 191 - case 'incorrect_cvc': 192 - $error_key = 'cvc'; 193 - $message = pht('Card CVC is invalid or incorrect.'); 194 - break; 195 - case 'expiry': 196 - case 'invalid_expiry_month': 197 - case 'invalid_expiry_year': 198 - $error_key = 'exp'; 199 - $message = pht('Card expiration date is invalid or incorrect.'); 200 - break; 201 - case 'card_declined': 202 - case 'expired_card': 203 - case 'duplicate_transaction': 204 - case 'processing_error': 205 - // these errors don't map well to field(s) being bad 206 - break; 207 - case 'invalid_amount': 208 - case 'missing': 209 - default: 210 - // these errors only happen if we (not the user) messed up so log it 211 - $error = sprintf('[Stripe Error] %s', $type); 212 - phlog($error); 213 - break; 214 - } 187 + switch ($short_code) { 188 + case 'error:incorrect_number': 189 + $error_key = 'number'; 190 + $message = pht('Invalid or incorrect credit card number.'); 191 + break; 192 + case 'error:incorrect_cvc': 193 + $error_key = 'cvc'; 194 + $message = pht('Card CVC is invalid or incorrect.'); 195 + break; 196 + $error_key = 'exp'; 197 + $message = pht('Card expiration date is invalid or incorrect.'); 198 + break; 199 + case 'error:invalid_expiry_month': 200 + case 'error:invalid_expiry_year': 201 + case 'error:invalid_cvc': 202 + case 'error:invalid_number': 203 + // NOTE: These should be translated into Phortune error codes earlier, 204 + // so we don't expect to receive them here. They are listed for clarity 205 + // and completeness. If we encounter one, we treat it as an unknown 206 + // error. 207 + break; 208 + case 'error:invalid_amount': 209 + case 'error:missing': 210 + case 'error:card_declined': 211 + case 'error:expired_card': 212 + case 'error:duplicate_transaction': 213 + case 'error:processing_error': 214 + default: 215 + // NOTE: These errors currently don't recevive a detailed message. 216 + // NOTE: We can also end up here with "http:nnn" messages. 215 217 216 - if ($error_key === null || isset($errors[$error_key])) { 217 - $errors[] = $message; 218 - } else { 219 - $errors[$error_key] = $message; 220 - } 218 + // TODO: At least some of these should have a better message, or be 219 + // translated into common errors above. 220 + break; 221 221 } 222 222 223 - return $errors; 223 + return null; 224 224 } 225 225 226 226 }
+12 -3
src/applications/phortune/storage/PhortunePaymentMethod.php
··· 11 11 const STATUS_FAILED = 'payment:failed'; 12 12 const STATUS_REMOVED = 'payment:removed'; 13 13 14 - protected $name; 14 + protected $name = ''; 15 15 protected $status; 16 16 protected $accountPHID; 17 17 protected $authorPHID; 18 - protected $expiresEpoch; 18 + protected $expires; 19 19 protected $metadata = array(); 20 + protected $brand; 21 + protected $lastFourDigits; 22 + protected $providerType; 23 + protected $providerDomain; 20 24 21 25 private $account; 22 26 ··· 47 51 } 48 52 49 53 public function getDescription() { 50 - return pht('Expires %s', date('m/y'), $this->getExpiresEpoch()); 54 + return '...'; 51 55 } 52 56 53 57 public function getMetadataValue($key, $default = null) { ··· 78 82 } 79 83 80 84 return head($accept); 85 + } 86 + 87 + public function setExpires($year, $month) { 88 + $this->expires = $year.'-'.$month; 89 + return $this; 81 90 } 82 91 83 92
+19 -15
src/applications/phortune/view/PhortuneCreditCardForm.php
··· 5 5 private $formID; 6 6 private $scripts = array(); 7 7 private $user; 8 + private $errors = array(); 8 9 9 10 private $cardNumberError; 10 11 private $cardCVCError; ··· 15 16 return $this; 16 17 } 17 18 18 - public function setCardExpirationError($card_expiration_error) { 19 - $this->cardExpirationError = $card_expiration_error; 20 - return $this; 21 - } 22 - 23 - public function setCardCVCError($card_cvc_error) { 24 - $this->cardCVCError = $card_cvc_error; 25 - return $this; 26 - } 27 - 28 - public function setCardNumberError($card_number_error) { 29 - $this->cardNumberError = $card_number_error; 19 + public function setErrors(array $errors) { 20 + $this->errors = $errors; 30 21 return $this; 31 22 } 32 23 ··· 63 54 ))); 64 55 } 65 56 57 + $errors = $this->errors; 58 + $e_number = isset($errors[PhortuneErrCode::ERR_CC_INVALID_NUMBER]) 59 + ? pht('Invalid') 60 + : true; 61 + 62 + $e_cvc = isset($errors[PhortuneErrCode::ERR_CC_INVALID_CVC]) 63 + ? pht('Invalid') 64 + : true; 65 + 66 + $e_expiry = isset($errors[PhortuneErrCode::ERR_CC_INVALID_EXPIRY]) 67 + ? pht('Invalid') 68 + : null; 69 + 66 70 $form 67 71 ->setID($form_id) 68 72 ->appendChild( ··· 85 89 ->setLabel('Card Number') 86 90 ->setDisableAutocomplete(true) 87 91 ->setSigil('number-input') 88 - ->setError($this->cardNumberError)) 92 + ->setError($e_number)) 89 93 ->appendChild( 90 94 id(new AphrontFormTextControl()) 91 95 ->setLabel('CVC') 92 96 ->setDisableAutocomplete(true) 93 97 ->setSigil('cvc-input') 94 - ->setError($this->cardCVCError)) 98 + ->setError($e_cvc)) 95 99 ->appendChild( 96 100 id(new PhortuneMonthYearExpiryControl()) 97 101 ->setLabel('Expiration') 98 102 ->setUser($this->user) 99 - ->setError($this->cardExpirationError)); 103 + ->setError($e_expiry)); 100 104 101 105 return $form; 102 106 }
+4
src/infrastructure/storage/patch/PhabricatorBuiltinPatchList.php
··· 1246 1246 'type' => 'sql', 1247 1247 'name' => $this->getPatchPath('20130423.updateexternalaccount.sql'), 1248 1248 ), 1249 + '20130423.phortunepaymentrevised.sql' => array( 1250 + 'type' => 'sql', 1251 + 'name' => $this->getPatchPath('20130423.phortunepaymentrevised.sql'), 1252 + ), 1249 1253 ); 1250 1254 } 1251 1255 }
+21 -35
webroot/rsrc/js/application/phortune/behavior-balanced-payment-form.js
··· 2 2 * @provides javelin-behavior-balanced-payment-form 3 3 * @requires javelin-behavior 4 4 * javelin-dom 5 - * javelin-json 6 - * javelin-workflow 7 5 * phortune-credit-card-form 8 6 */ 9 7 10 8 JX.behavior('balanced-payment-form', function(config) { 11 9 balanced.init(config.balancedMarketplaceURI); 12 10 13 - var root = JX.$(config.formID); 14 - var ccform = new JX.PhortuneCreditCardForm(root); 15 - 16 - var onsubmit = function(e) { 17 - e.kill(); 11 + var ccform = new JX.PhortuneCreditCardForm(JX.$(config.formID), onsubmit); 18 12 19 - var cardData = ccform.getCardData(); 13 + function onsubmit(card_data) { 20 14 var errors = []; 21 15 22 - if (!balanced.card.isCardNumberValid(cardData.number)) { 23 - errors.push('number'); 16 + if (!balanced.card.isCardNumberValid(card_data.number)) { 17 + errors.push('cc:invalid:number'); 24 18 } 25 19 26 - if (!balanced.card.isSecurityCodeValid(cardData.number, cardData.cvc)) { 27 - errors.push('cvc'); 20 + if (!balanced.card.isSecurityCodeValid(card_data.number, card_data.cvc)) { 21 + errors.push('cc:invalid:cvc'); 28 22 } 29 23 30 - if (!balanced.card.isExpiryValid(cardData.month, cardData.year)) { 31 - errors.push('expiry'); 24 + if (!balanced.card.isExpiryValid(card_data.month, card_data.year)) { 25 + errors.push('cc:invalid:expiry'); 32 26 } 33 27 34 28 if (errors.length) { 35 - JX.Workflow 36 - .newFromForm(root, {cardErrors: JX.JSON.stringify(errors)}) 37 - .start(); 29 + ccform.submitForm(errors); 38 30 return; 39 31 } 40 32 41 33 var data = { 42 - card_number: cardData.number, 43 - security_code: cardData.cvc, 44 - expiration_month: cardData.month, 45 - expiration_year: cardData.year 34 + card_number: card_data.number, 35 + security_code: card_data.cvc, 36 + expiration_month: card_data.month, 37 + expiration_year: card_data.year 46 38 }; 47 39 48 40 balanced.card.create(data, onresponse); 49 41 } 50 42 51 - var onresponse = function(response) { 52 - 43 + function onresponse(response) { 44 + var token = null; 53 45 var errors = []; 46 + 54 47 if (response.error) { 55 - errors = [response.error.type]; 48 + errors = ['cc:balanced:error:' + response.error.type]; 56 49 } else if (response.status != 201) { 57 - errors = ['balanced:' + response.status]; 50 + errors = ['cc:balanced:http:' + response.status]; 51 + } else { 52 + token = response.data.uri; 58 53 } 59 54 60 - var params = { 61 - cardErrors: JX.JSON.stringify(errors), 62 - balancedCardData: JX.JSON.stringify(response.data) 63 - }; 64 - 65 - JX.Workflow 66 - .newFromForm(root, params) 67 - .start(); 55 + ccform.submitForm(errors, {balancedMarketplaceURI: token}); 68 56 } 69 - 70 - JX.DOM.listen(root, 'submit', null, onsubmit); 71 57 });
+19 -35
webroot/rsrc/js/application/phortune/behavior-stripe-payment-form.js
··· 2 2 * @provides javelin-behavior-stripe-payment-form 3 3 * @requires javelin-behavior 4 4 * javelin-dom 5 - * javelin-json 6 - * javelin-workflow 7 5 * phortune-credit-card-form 8 6 */ 9 7 10 8 JX.behavior('stripe-payment-form', function(config) { 11 9 Stripe.setPublishableKey(config.stripePublishableKey); 12 10 13 - var root = JX.$(config.formID); 14 - var ccform = new JX.PhortuneCreditCardForm(root); 15 - 16 - var onsubmit = function(e) { 17 - e.kill(); 11 + var ccform = new JX.PhortuneCreditCardForm(JX.$(config.formID), onsubmit); 18 12 19 - // validate the card data with Stripe client API and submit the form 20 - // with any detected errors 21 - var cardData = ccform.getCardData(); 13 + function onsubmit(card_data) { 22 14 var errors = []; 23 15 24 - if (!Stripe.validateCardNumber(cardData.number)) { 25 - errors.push('number'); 16 + if (!Stripe.validateCardNumber(card_data.number)) { 17 + errors.push('cc:invalid:number'); 26 18 } 27 19 28 - if (!Stripe.validateCVC(cardData.cvc)) { 29 - errors.push('cvc'); 20 + if (!Stripe.validateCVC(card_data.cvc)) { 21 + errors.push('cc:invalid:cvc'); 30 22 } 31 23 32 - if (!Stripe.validateExpiry(cardData.month, cardData.year)) { 33 - errors.push('expiry'); 24 + if (!Stripe.validateExpiry(card_data.month, card_data.year)) { 25 + errors.push('cc:invalid:expiry'); 34 26 } 35 27 36 28 if (errors.length) { 37 - JX.Workflow 38 - .newFromForm(root, {cardErrors: JX.JSON.stringify(errors)}) 39 - .start(); 29 + ccform.submitForm(errors); 40 30 return; 41 31 } 42 32 43 33 var data = { 44 - number: cardData.number, 45 - cvc: cardData.cvc, 46 - exp_month: cardData.month, 47 - exp_year: cardData.year 34 + number: card_data.number, 35 + cvc: card_data.cvc, 36 + exp_month: card_data.month, 37 + exp_year: card_data.year 48 38 }; 49 39 50 40 Stripe.createToken(data, onresponse); 51 41 } 52 42 53 - var onresponse = function(status, response) { 43 + function onresponse(status, response) { 54 44 var errors = []; 55 45 var token = null; 56 - if (response.error) { 57 - errors = [response.error.type]; 46 + if (status != 200) { 47 + errors.push('cc:stripe:http:' + status); 48 + } else if (response.error) { 49 + errors.push('cc:stripe:error:' + response.error.type); 58 50 } else { 59 51 token = response.id; 60 52 } 61 53 62 - var params = { 63 - cardErrors: JX.JSON.stringify(errors), 64 - stripeToken: token 65 - }; 66 - 67 - JX.Workflow 68 - .newFromForm(root, params) 69 - .start(); 54 + ccform.submitForm(errors, {stripeCardToken: token}); 70 55 } 71 56 72 - JX.DOM.listen(root, 'submit', null, onsubmit); 73 57 });
+27 -6
webroot/rsrc/js/application/phortune/phortune-credit-card-form.js
··· 2 2 * @provides phortune-credit-card-form 3 3 * @requires javelin-install 4 4 * javelin-dom 5 + * javelin-json 6 + * javelin-workflow 7 + * javelin-util 8 + * @javelin 5 9 */ 6 10 7 11 /** ··· 9 13 * 10 14 * To construct an object for a form: 11 15 * 12 - * new JX.PhortuneCreditCardForm(form_root_node); 16 + * new JX.PhortuneCreditCardForm(form_root_node, submit_callback); 13 17 * 14 - * To read card data from a form: 15 - * 16 - * var data = ccform.getCardData(); 17 18 */ 18 19 JX.install('PhortuneCreditCardForm', { 19 - construct : function(root) { 20 + construct : function(root, onsubmit) { 20 21 this._root = root; 22 + this._submitCallback = onsubmit; 23 + JX.DOM.listen(root, 'submit', null, JX.bind(this, this._onsubmit)); 21 24 }, 22 25 23 26 members : { 24 27 _root : null, 28 + _submitCallback : null, 25 29 26 - getCardData : function() { 30 + _getCardData : function() { 27 31 var root = this._root; 28 32 29 33 return { ··· 32 36 month : JX.DOM.find(root, 'select', 'month-input' ).value, 33 37 year : JX.DOM.find(root, 'select', 'year-input' ).value 34 38 }; 39 + }, 40 + 41 + submitForm : function(errors, token) { 42 + var params = { 43 + errors: JX.JSON.stringify(errors), 44 + token: JX.JSON.stringify(token || {}) 45 + }; 46 + 47 + JX.Workflow 48 + .newFromForm(this._root, params) 49 + .start(); 50 + }, 51 + 52 + _onsubmit : function(e) { 53 + e.kill(); 54 + this._submitCallback(this._getCardData()); 35 55 } 56 + 36 57 } 37 58 38 59 });