@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

Phortune v0.1: add payment methods

Summary:
Hook @btrahan's Stripe form to the rest of Phortune.

- Users can add payment methods.
- They are saved to Stripe and associated with PhortunePaymentMethods on our side.
- Payment methods appear on account overview.

Test Plan:
{F37548}
{F37549}
{F37550}

Reviewers: chad, btrahan

Reviewed By: btrahan

CC: aran

Maniphest Tasks: T2787

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

+521 -184
+14
resources/sql/patches/20130323.phortunepayment.sql
··· 1 + CREATE TABLE {$NAMESPACE}_phortune.phortune_paymentmethod ( 2 + id INT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY, 3 + phid VARCHAR(64) NOT NULL COLLATE utf8_bin, 4 + name VARCHAR(255) NOT NULL, 5 + status VARCHAR(64) NOT NULL COLLATE utf8_bin, 6 + accountPHID VARCHAR(64) NOT NULL COLLATE utf8_bin, 7 + authorPHID VARCHAR(64) NOT NULL COLLATE utf8_bin, 8 + expiresEpoch INT UNSIGNED, 9 + metadata LONGTEXT NOT NULL COLLATE utf8_bin, 10 + dateCreated INT UNSIGNED NOT NULL, 11 + dateModified INT UNSIGNED NOT NULL, 12 + UNIQUE KEY `key_phid` (phid), 13 + KEY `key_account` (accountPHID, status) 14 + ) ENGINE=InnoDB DEFAULT CHARSET=utf8;
+7 -5
src/__phutil_library_map__.php
··· 1359 1359 'PhabricatorStorageManagementUpgradeWorkflow' => 'infrastructure/storage/management/workflow/PhabricatorStorageManagementUpgradeWorkflow.php', 1360 1360 'PhabricatorStorageManagementWorkflow' => 'infrastructure/storage/management/workflow/PhabricatorStorageManagementWorkflow.php', 1361 1361 'PhabricatorStoragePatch' => 'infrastructure/storage/management/PhabricatorStoragePatch.php', 1362 + 'PhabricatorStripeConfigOptions' => 'applications/phortune/option/PhabricatorStripeConfigOptions.php', 1362 1363 'PhabricatorSubscribableInterface' => 'applications/subscriptions/interface/PhabricatorSubscribableInterface.php', 1363 1364 'PhabricatorSubscribersQuery' => 'applications/subscriptions/query/PhabricatorSubscribersQuery.php', 1364 1365 'PhabricatorSubscriptionsEditController' => 'applications/subscriptions/controller/PhabricatorSubscriptionsEditController.php', ··· 1540 1541 'PhortuneLandingController' => 'applications/phortune/controller/PhortuneLandingController.php', 1541 1542 'PhortuneMonthYearExpiryControl' => 'applications/phortune/control/PhortuneMonthYearExpiryControl.php', 1542 1543 'PhortunePaymentMethod' => 'applications/phortune/storage/PhortunePaymentMethod.php', 1544 + 'PhortunePaymentMethodEditController' => 'applications/phortune/controller/PhortunePaymentMethodEditController.php', 1543 1545 'PhortunePaymentMethodListController' => 'applications/phortune/controller/PhortunePaymentMethodListController.php', 1546 + 'PhortunePaymentMethodQuery' => 'applications/phortune/query/PhortunePaymentMethodQuery.php', 1544 1547 'PhortunePaymentMethodViewController' => 'applications/phortune/controller/PhortunePaymentMethodViewController.php', 1545 1548 'PhortuneProduct' => 'applications/phortune/storage/PhortuneProduct.php', 1546 1549 'PhortunePurchase' => 'applications/phortune/storage/PhortunePurchase.php', 1547 - 'PhortuneStripeBaseController' => 'applications/phortune/stripe/controller/PhortuneStripeBaseController.php', 1548 - 'PhortuneStripePaymentFormView' => 'applications/phortune/stripe/view/PhortuneStripePaymentFormView.php', 1549 - 'PhortuneStripeTestPaymentFormController' => 'applications/phortune/stripe/controller/PhortuneStripeTestPaymentFormController.php', 1550 + 'PhortuneStripePaymentFormView' => 'applications/phortune/view/PhortuneStripePaymentFormView.php', 1550 1551 'PhrictionActionConstants' => 'applications/phriction/constants/PhrictionActionConstants.php', 1551 1552 'PhrictionChangeType' => 'applications/phriction/constants/PhrictionChangeType.php', 1552 1553 'PhrictionConstants' => 'applications/phriction/constants/PhrictionConstants.php', ··· 2977 2978 'PhabricatorStorageManagementStatusWorkflow' => 'PhabricatorStorageManagementWorkflow', 2978 2979 'PhabricatorStorageManagementUpgradeWorkflow' => 'PhabricatorStorageManagementWorkflow', 2979 2980 'PhabricatorStorageManagementWorkflow' => 'PhutilArgumentWorkflow', 2981 + 'PhabricatorStripeConfigOptions' => 'PhabricatorApplicationConfigOptions', 2980 2982 'PhabricatorSubscribersQuery' => 'PhabricatorQuery', 2981 2983 'PhabricatorSubscriptionsEditController' => 'PhabricatorController', 2982 2984 'PhabricatorSubscriptionsEditor' => 'PhabricatorEditor', ··· 3194 3196 0 => 'PhortuneDAO', 3195 3197 1 => 'PhabricatorPolicyInterface', 3196 3198 ), 3199 + 'PhortunePaymentMethodEditController' => 'PhortuneController', 3197 3200 'PhortunePaymentMethodListController' => 'PhabricatorController', 3201 + 'PhortunePaymentMethodQuery' => 'PhabricatorCursorPagedPolicyAwareQuery', 3198 3202 'PhortunePaymentMethodViewController' => 'PhabricatorController', 3199 3203 'PhortuneProduct' => 'PhortuneDAO', 3200 3204 'PhortunePurchase' => 'PhortuneDAO', 3201 - 'PhortuneStripeBaseController' => 'PhabricatorController', 3202 3205 'PhortuneStripePaymentFormView' => 'AphrontView', 3203 - 'PhortuneStripeTestPaymentFormController' => 'PhortuneStripeBaseController', 3204 3206 'PhrictionActionConstants' => 'PhrictionConstants', 3205 3207 'PhrictionChangeType' => 'PhrictionConstants', 3206 3208 'PhrictionContent' =>
+3 -6
src/applications/phortune/application/PhabricatorApplicationPhortune.php
··· 32 32 '' => 'PhortuneLandingController', 33 33 '(?P<accountID>\d+)/' => array( 34 34 '' => 'PhortuneAccountViewController', 35 + 'paymentmethod/' => array( 36 + 'edit/' => 'PhortunePaymentMethodEditController', 37 + ), 35 38 ), 36 - 37 39 'account/' => array( 38 40 '' => 'PhortuneAccountListController', 39 41 'edit/(?:(?P<id>\d+)/)?' => 'PhortuneAccountEditController', 40 - ), 41 - 'paymentmethod/' => array( 42 - '' => 'PhortunePaymentMethodListController', 43 - 'view/(?P<id>\d+)/' => 'PhortunePaymentMethodViewController', 44 - 'edit/(?:(?P<id>\d+)/)?' => 'PhortunePaymentMethodEditController', 45 42 ), 46 43 'stripe/' => array( 47 44 'testpaymentform/' => 'PhortuneStripeTestPaymentFormController',
+34
src/applications/phortune/controller/PhortuneAccountViewController.php
··· 95 95 ->setNoDataString( 96 96 pht('No payment methods associated with this account.')); 97 97 98 + $methods = id(new PhortunePaymentMethodQuery()) 99 + ->setViewer($user) 100 + ->withAccountPHIDs(array($account->getPHID())) 101 + ->withStatus(PhortunePaymentMethodQuery::STATUS_OPEN) 102 + ->execute(); 103 + 104 + if ($methods) { 105 + $this->loadHandles(mpull($methods, 'getAuthorPHID')); 106 + } 107 + 108 + foreach ($methods as $method) { 109 + $item = new PhabricatorObjectItemView(); 110 + $item->setHeader($method->getName()); 111 + 112 + switch ($method->getStatus()) { 113 + case PhortunePaymentMethod::STATUS_ACTIVE: 114 + $item->addAttribute(pht('Active')); 115 + $item->setBarColor('green'); 116 + break; 117 + } 118 + 119 + $item->addAttribute( 120 + pht( 121 + 'Added %s by %s', 122 + phabricator_datetime($method->getDateCreated(), $user), 123 + $this->getHandle($method->getAuthorPHID())->renderLink())); 124 + 125 + if ($method->getExpiresEpoch() < time() + (60 * 60 * 24 * 30)) { 126 + $item->addAttribute(pht('Expires Soon!')); 127 + } 128 + 129 + $list->addItem($item); 130 + } 131 + 98 132 return array( 99 133 $header, 100 134 $actions,
+298
src/applications/phortune/controller/PhortunePaymentMethodEditController.php
··· 1 + <?php 2 + 3 + final class PhortunePaymentMethodEditController 4 + extends PhortuneController { 5 + 6 + private $accountID; 7 + 8 + public function willProcessRequest(array $data) { 9 + $this->accountID = $data['accountID']; 10 + } 11 + 12 + /** 13 + * @phutil-external-symbol class Stripe_Token 14 + * @phutil-external-symbol class Stripe_Customer 15 + */ 16 + public function processRequest() { 17 + $request = $this->getRequest(); 18 + $user = $request->getUser(); 19 + 20 + $stripe_publishable_key = PhabricatorEnv::getEnvConfig( 21 + 'stripe.publishable-key'); 22 + if (!$stripe_publishable_key) { 23 + throw new Exception( 24 + "Stripe publishable API key (`stripe.publishable-key`) is ". 25 + "not configured."); 26 + } 27 + 28 + $stripe_secret_key = PhabricatorEnv::getEnvConfig('stripe.secret-key'); 29 + if (!$stripe_secret_key) { 30 + throw new Exception( 31 + "Stripe secret API kye (`stripe.secret-key`) is not configured."); 32 + } 33 + 34 + $account = id(new PhortuneAccountQuery()) 35 + ->setViewer($user) 36 + ->withIDs(array($this->accountID)) 37 + ->executeOne(); 38 + if (!$account) { 39 + return new Aphront404Response(); 40 + } 41 + 42 + $account_uri = $this->getApplicationURI($account->getID().'/'); 43 + 44 + $e_card_number = true; 45 + $e_card_cvc = true; 46 + $e_card_exp = true; 47 + 48 + $errors = array(); 49 + if ($request->isFormPost()) { 50 + $card_errors = $request->getStr('cardErrors'); 51 + $stripe_token = $request->getStr('stripeToken'); 52 + if ($card_errors) { 53 + $raw_errors = json_decode($card_errors); 54 + list($e_card_number, 55 + $e_card_cvc, 56 + $e_card_exp, 57 + $messages) = $this->parseRawErrors($raw_errors); 58 + $errors = array_merge($errors, $messages); 59 + } else if (!$stripe_token) { 60 + $errors[] = pht('There was an unknown error processing your card.'); 61 + } 62 + 63 + if (!$errors) { 64 + $root = dirname(phutil_get_library_root('phabricator')); 65 + require_once $root.'/externals/stripe-php/lib/Stripe.php'; 66 + 67 + try { 68 + // First, make sure the token is valid. 69 + $info = id(new Stripe_Token()) 70 + ->retrieve($stripe_token, $stripe_secret_key); 71 + 72 + // Then, we need to create a Customer in order to be able to charge 73 + // the card more than once. We create one Customer for each card; 74 + // they do not map to PhortuneAccounts because we allow an account to 75 + // have more than one active card. 76 + $customer = Stripe_Customer::create( 77 + array( 78 + 'card' => $stripe_token, 79 + 'description' => $account->getPHID().':'.$user->getUserName(), 80 + ), $stripe_secret_key); 81 + 82 + $card = $info->card; 83 + } catch (Exception $ex) { 84 + phlog($ex); 85 + $errors[] = pht( 86 + 'There was an error communicating with the payments backend.'); 87 + } 88 + 89 + if (!$errors) { 90 + $payment_method = id(new PhortunePaymentMethod()) 91 + ->setAccountPHID($account->getPHID()) 92 + ->setAuthorPHID($user->getPHID()) 93 + ->setName($card->type.' / '.$card->last4) 94 + ->setStatus(PhortunePaymentMethod::STATUS_ACTIVE) 95 + ->setExpiresEpoch(strtotime($card->exp_year.'-'.$card->exp_month)) 96 + ->setMetadata( 97 + array( 98 + 'type' => 'stripe.customer', 99 + 'stripeCustomerID' => $customer->id, 100 + 'stripeTokenID' => $stripe_token, 101 + )) 102 + ->save(); 103 + 104 + $save_uri = new PhutilURI($account_uri); 105 + $save_uri->setFragment('payment'); 106 + 107 + return id(new AphrontRedirectResponse())->setURI($save_uri); 108 + } 109 + } 110 + 111 + $dialog = id(new AphrontDialogView()) 112 + ->setUser($user) 113 + ->setTitle(pht('Error Adding Card')) 114 + ->appendChild(id(new AphrontErrorView())->setErrors($errors)) 115 + ->addCancelButton($request->getRequestURI()); 116 + 117 + return id(new AphrontDialogResponse())->setDialog($dialog); 118 + } 119 + 120 + if ($errors) { 121 + $errors = id(new AphrontErrorView()) 122 + ->setErrors($errors); 123 + } 124 + 125 + $header = id(new PhabricatorHeaderView()) 126 + ->setHeader(pht('Add New Payment Method')); 127 + 128 + $form_id = celerity_generate_unique_node_id(); 129 + require_celerity_resource('stripe-payment-form-css'); 130 + require_celerity_resource('aphront-tooltip-css'); 131 + Javelin::initBehavior('phabricator-tooltips'); 132 + 133 + $form = id(new AphrontFormView()) 134 + ->setID($form_id) 135 + ->setUser($user) 136 + ->setWorkflow(true) 137 + ->setAction($request->getRequestURI()) 138 + ->appendChild( 139 + id(new AphrontFormMarkupControl()) 140 + ->setLabel('') 141 + ->setValue( 142 + javelin_tag( 143 + 'div', 144 + array( 145 + 'class' => 'credit-card-logos', 146 + 'sigil' => 'has-tooltip', 147 + 'meta' => array( 148 + 'tip' => 'We support Visa, Mastercard, American Express, '. 149 + 'Discover, JCB, and Diners Club.', 150 + 'size' => 440, 151 + ) 152 + )))) 153 + ->appendChild( 154 + id(new AphrontFormTextControl()) 155 + ->setLabel('Card Number') 156 + ->setDisableAutocomplete(true) 157 + ->setSigil('number-input') 158 + ->setError($e_card_number)) 159 + ->appendChild( 160 + id(new AphrontFormTextControl()) 161 + ->setLabel('CVC') 162 + ->setDisableAutocomplete(true) 163 + ->setSigil('cvc-input') 164 + ->setError($e_card_cvc)) 165 + ->appendChild( 166 + id(new PhortuneMonthYearExpiryControl()) 167 + ->setLabel('Expiration') 168 + ->setUser($user) 169 + ->setError($e_card_exp)) 170 + ->appendChild( 171 + javelin_tag( 172 + 'input', 173 + array( 174 + 'hidden' => true, 175 + 'name' => 'stripeToken', 176 + 'sigil' => 'stripe-token-input', 177 + ))) 178 + ->appendChild( 179 + javelin_tag( 180 + 'input', 181 + array( 182 + 'hidden' => true, 183 + 'name' => 'cardErrors', 184 + 'sigil' => 'card-errors-input' 185 + ))) 186 + ->appendChild( 187 + phutil_tag( 188 + 'input', 189 + array( 190 + 'hidden' => true, 191 + 'name' => 'stripeKey', 192 + 'value' => $stripe_publishable_key, 193 + ))) 194 + ->appendChild( 195 + id(new AphrontFormSubmitControl()) 196 + ->setValue('Add Payment Method') 197 + ->addCancelButton($account_uri)); 198 + 199 + Javelin::initBehavior( 200 + 'stripe-payment-form', 201 + array( 202 + 'stripePublishKey' => $stripe_publishable_key, 203 + 'root' => $form_id, 204 + )); 205 + 206 + $title = pht('Add Payment Method'); 207 + 208 + $crumbs = $this->buildApplicationCrumbs(); 209 + $crumbs->addCrumb( 210 + id(new PhabricatorCrumbView()) 211 + ->setName(pht('Account')) 212 + ->setHref($account_uri)); 213 + $crumbs->addCrumb( 214 + id(new PhabricatorCrumbView()) 215 + ->setName(pht('Payment Methods')) 216 + ->setHref($request->getRequestURI())); 217 + 218 + return 219 + $this->buildStandardPageResponse( 220 + array( 221 + $crumbs, 222 + $header, 223 + $errors, 224 + $form, 225 + ), 226 + array( 227 + 'title' => $title, 228 + 'device' => true, 229 + 'dust' => true, 230 + )); 231 + } 232 + 233 + /** 234 + * Stripe JS and calls to Stripe handle all errors with processing this 235 + * form. This function takes the raw errors - in the form of an array 236 + * where each elementt is $type => $message - and figures out what if 237 + * any fields were invalid and pulls the messages into a flat object. 238 + * 239 + * See https://stripe.com/docs/api#errors for more information on possible 240 + * errors. 241 + */ 242 + private function parseRawErrors($errors) { 243 + $card_number_error = null; 244 + $card_cvc_error = null; 245 + $card_expiration_error = null; 246 + $messages = array(); 247 + foreach ($errors as $index => $error) { 248 + $type = key($error); 249 + $msg = reset($error); 250 + $messages[] = $msg; 251 + switch ($type) { 252 + case 'number': 253 + case 'invalid_number': 254 + case 'incorrect_number': 255 + $card_number_error = pht('Invalid'); 256 + break; 257 + case 'cvc': 258 + case 'invalid_cvc': 259 + case 'incorrect_cvc': 260 + $card_cvc_error = pht('Invalid'); 261 + break; 262 + case 'expiry': 263 + case 'invalid_expiry_month': 264 + case 'invalid_expiry_year': 265 + $card_expiration_error = pht('Invalid'); 266 + break; 267 + case 'card_declined': 268 + case 'expired_card': 269 + case 'duplicate_transaction': 270 + case 'processing_error': 271 + // these errors don't map well to field(s) being bad 272 + break; 273 + case 'invalid_amount': 274 + case 'missing': 275 + default: 276 + // these errors only happen if we (not the user) messed up so log it 277 + $error = sprintf( 278 + 'error_type: %s error_message: %s', 279 + $type, 280 + $msg); 281 + $this->logStripeError($error); 282 + break; 283 + } 284 + } 285 + 286 + return array( 287 + $card_number_error, 288 + $card_cvc_error, 289 + $card_expiration_error, 290 + $messages 291 + ); 292 + } 293 + 294 + private function logStripeError($message) { 295 + phlog('STRIPE-ERROR '.$message); 296 + } 297 + 298 + }
+25
src/applications/phortune/option/PhabricatorStripeConfigOptions.php
··· 1 + <?php 2 + 3 + final class PhabricatorStripeConfigOptions 4 + extends PhabricatorApplicationConfigOptions { 5 + 6 + public function getName() { 7 + return pht("Integration with Stripe"); 8 + } 9 + 10 + public function getDescription() { 11 + return pht("Configure Stripe payments."); 12 + } 13 + 14 + public function getOptions() { 15 + return array( 16 + $this->newOption('stripe.publishable-key', 'string', null) 17 + ->setDescription( 18 + pht('Stripe publishable key.')), 19 + $this->newOption('stripe.secret-key', 'string', null) 20 + ->setDescription( 21 + pht('Stripe secret key.')), 22 + ); 23 + } 24 + 25 + }
+117
src/applications/phortune/query/PhortunePaymentMethodQuery.php
··· 1 + <?php 2 + 3 + final class PhortunePaymentMethodQuery 4 + extends PhabricatorCursorPagedPolicyAwareQuery { 5 + 6 + private $ids; 7 + private $phids; 8 + private $accountPHIDs; 9 + 10 + const STATUS_ANY = 'status-any'; 11 + const STATUS_OPEN = 'status-open'; 12 + private $status = self::STATUS_ANY; 13 + 14 + public function withIDs(array $ids) { 15 + $this->ids = $ids; 16 + return $this; 17 + } 18 + 19 + public function withPHIDs(array $phids) { 20 + $this->phids = $phids; 21 + return $this; 22 + } 23 + 24 + public function withAccountPHIDs(array $phids) { 25 + $this->accountPHIDs = $phids; 26 + return $this; 27 + } 28 + 29 + public function withStatus($status) { 30 + $this->status = $status; 31 + return $this; 32 + } 33 + 34 + protected function loadPage() { 35 + $table = new PhortunePaymentMethod(); 36 + $conn = $table->establishConnection('r'); 37 + 38 + $rows = queryfx_all( 39 + $conn, 40 + 'SELECT * FROM %T %Q %Q %Q', 41 + $table->getTableName(), 42 + $this->buildWhereClause($conn), 43 + $this->buildOrderClause($conn), 44 + $this->buildLimitClause($conn)); 45 + 46 + return $table->loadAllFromArray($rows); 47 + } 48 + 49 + protected function willFilterPage(array $methods) { 50 + if (!$methods) { 51 + return array(); 52 + } 53 + 54 + $accounts = id(new PhortuneAccountQuery()) 55 + ->setViewer($this->getViewer()) 56 + ->withPHIDs(mpull($methods, 'getAccountPHID')) 57 + ->execute(); 58 + $accounts = mpull($accounts, null, 'getPHID'); 59 + 60 + foreach ($methods as $key => $method) { 61 + $account = idx($accounts, $method->getAccountPHID()); 62 + if (!$account) { 63 + unset($methods[$key]); 64 + continue; 65 + } 66 + $method->attachAccount($account); 67 + } 68 + 69 + return $methods; 70 + } 71 + 72 + private function buildWhereClause(AphrontDatabaseConnection $conn) { 73 + $where = array(); 74 + 75 + if ($this->ids) { 76 + $where[] = qsprintf( 77 + $conn, 78 + 'id IN (%Ld)', 79 + $this->ids); 80 + } 81 + 82 + if ($this->phids) { 83 + $where[] = qsprintf( 84 + $conn, 85 + 'phid IN (%Ls)', 86 + $this->phids); 87 + } 88 + 89 + if ($this->accountPHIDs) { 90 + $where[] = qsprintf( 91 + $conn, 92 + 'accountPHID IN (%Ls)', 93 + $this->accountPHIDs); 94 + } 95 + 96 + switch ($this->status) { 97 + case self::STATUS_ANY; 98 + break; 99 + case self::STATUS_OPEN: 100 + $where[] = qsprintf( 101 + $conn, 102 + 'status in (%Ls)', 103 + array( 104 + PhortunePaymentMethod::STATUS_ACTIVE, 105 + PhortunePaymentMethod::STATUS_FAILED, 106 + )); 107 + break; 108 + default: 109 + throw new Exception("Unknown status '{$this->status}'!"); 110 + } 111 + 112 + $where[] = $this->buildPagingClause($conn); 113 + 114 + return $this->formatWhereClause($where); 115 + } 116 + 117 + }
+6
src/applications/phortune/storage/PhortunePaymentMethod.php
··· 7 7 final class PhortunePaymentMethod extends PhortuneDAO 8 8 implements PhabricatorPolicyInterface { 9 9 10 + const STATUS_ACTIVE = 'payment:active'; 11 + const STATUS_FAILED = 'payment:failed'; 12 + const STATUS_REMOVED = 'payment:removed'; 13 + 10 14 protected $name; 15 + protected $status; 11 16 protected $accountPHID; 12 17 protected $authorPHID; 18 + protected $expiresEpoch; 13 19 protected $metadata; 14 20 15 21 private $account;
-17
src/applications/phortune/stripe/controller/PhortuneStripeBaseController.php
··· 1 - <?php 2 - 3 - abstract class PhortuneStripeBaseController extends PhabricatorController { 4 - 5 - public function buildStandardPageResponse($view, array $data) { 6 - $page = $this->buildStandardPageView(); 7 - 8 - $page->setApplicationName('Phortune - Stripe'); 9 - $page->setBaseURI('/phortune/stripe/'); 10 - $page->setTitle(idx($data, 'title')); 11 - $page->appendChild($view); 12 - 13 - $response = new AphrontWebpageResponse(); 14 - return $response->setContent($page->render()); 15 - } 16 - 17 - }
-146
src/applications/phortune/stripe/controller/PhortuneStripeTestPaymentFormController.php
··· 1 - <?php 2 - 3 - final class PhortuneStripeTestPaymentFormController 4 - extends PhortuneStripeBaseController { 5 - public function processRequest() { 6 - $request = $this->getRequest(); 7 - $user = $request->getUser(); 8 - $title = 'Test Payment Form'; 9 - $error_view = null; 10 - $card_number_error = null; 11 - $card_cvc_error = null; 12 - $card_expiration_error = null; 13 - $stripe_key = $request->getStr('stripeKey'); 14 - if (!$stripe_key) { 15 - $error_view = id(new AphrontErrorView()) 16 - ->setTitle('Missing stripeKey parameter in URI'); 17 - } 18 - 19 - if (!$error_view && $request->isFormPost()) { 20 - $card_errors = $request->getStr('cardErrors'); 21 - $stripe_token = $request->getStr('stripeToken'); 22 - if ($card_errors) { 23 - $raw_errors = json_decode($card_errors); 24 - list($card_number_error, 25 - $card_cvc_error, 26 - $card_expiration_error, 27 - $messages) = $this->parseRawErrors($raw_errors); 28 - $error_view = id(new AphrontErrorView()) 29 - ->setTitle('There were errors processing your card.') 30 - ->setErrors($messages); 31 - } else if (!$stripe_token) { 32 - // this shouldn't happen, so show the user a very generic error 33 - // message and log that this error occurred...! 34 - $error_view = id(new AphrontErrorView()) 35 - ->setTitle('There was an unknown error processing your card.') 36 - ->setErrors(array('Please try again.')); 37 - $error = 'payment form submitted but no stripe token and no errors'; 38 - $this->logStripeError($error); 39 - } else { 40 - // success -- do something with $stripe_token!! 41 - } 42 - } else if (!$error_view) { 43 - $error_view = id(new AphrontErrorView()) 44 - ->setSeverity(AphrontErrorView::SEVERITY_NOTICE) 45 - ->setTitle( 46 - 'If you are using a test stripe key, use 4242424242424242, '. 47 - 'any three digits for CVC, and any valid expiration date to '. 48 - 'test!'); 49 - } 50 - 51 - $view = id(new AphrontPanelView()) 52 - ->setWidth(AphrontPanelView::WIDTH_FORM) 53 - ->setHeader($title); 54 - 55 - $form = id(new PhortuneStripePaymentFormView()) 56 - ->setUser($user) 57 - ->setStripeKey($stripe_key) 58 - ->setCardNumberError($card_number_error) 59 - ->setCardCVCError($card_cvc_error) 60 - ->setCardExpirationError($card_expiration_error); 61 - 62 - $view->appendChild($form); 63 - 64 - return 65 - $this->buildStandardPageResponse( 66 - array( 67 - $error_view, 68 - $view, 69 - ), 70 - array( 71 - 'title' => $title, 72 - )); 73 - } 74 - 75 - /** 76 - * Stripe JS and calls to Stripe handle all errors with processing this 77 - * form. This function takes the raw errors - in the form of an array 78 - * where each elementt is $type => $message - and figures out what if 79 - * any fields were invalid and pulls the messages into a flat object. 80 - * 81 - * See https://stripe.com/docs/api#errors for more information on possible 82 - * errors. 83 - */ 84 - private function parseRawErrors($errors) { 85 - $card_number_error = null; 86 - $card_cvc_error = null; 87 - $card_expiration_error = null; 88 - $messages = array(); 89 - foreach ($errors as $index => $error) { 90 - $type = key($error); 91 - $msg = reset($error); 92 - $messages[] = $msg; 93 - switch ($type) { 94 - case 'number': 95 - case 'invalid_number': 96 - case 'incorrect_number': 97 - $card_number_error = true; 98 - break; 99 - case 'cvc': 100 - case 'invalid_cvc': 101 - case 'incorrect_cvc': 102 - $card_cvc_error = true; 103 - break; 104 - case 'expiry': 105 - case 'invalid_expiry_month': 106 - case 'invalid_expiry_year': 107 - $card_expiration_error = true; 108 - break; 109 - case 'card_declined': 110 - case 'expired_card': 111 - case 'duplicate_transaction': 112 - case 'processing_error': 113 - // these errors don't map well to field(s) being bad 114 - break; 115 - case 'invalid_amount': 116 - case 'missing': 117 - default: 118 - // these errors only happen if we (not the user) messed up so log it 119 - $error = sprintf( 120 - 'error_type: %s error_message: %s', 121 - $type, 122 - $msg); 123 - $this->logStripeError($error); 124 - break; 125 - } 126 - } 127 - 128 - // append a helpful "fix this" to the messages to be displayed to the user 129 - $messages[] = pht( 130 - 'Please fix these errors and try again.', 131 - count($messages)); 132 - 133 - return array( 134 - $card_number_error, 135 - $card_cvc_error, 136 - $card_expiration_error, 137 - $messages 138 - ); 139 - } 140 - 141 - private function logStripeError($message) { 142 - phlog('STRIPE-ERROR '.$message); 143 - } 144 - 145 - 146 - }
+2 -2
src/applications/phortune/stripe/view/PhortuneStripePaymentFormView.php src/applications/phortune/view/PhortuneStripePaymentFormView.php
··· 39 39 } 40 40 41 41 public function render() { 42 - $form_id = celerity_generate_unique_node_id(); 42 + $form_id = celerity_generate_unique_node_id(); 43 43 require_celerity_resource('stripe-payment-form-css'); 44 44 require_celerity_resource('aphront-tooltip-css'); 45 45 Javelin::initBehavior('phabricator-tooltips'); ··· 105 105 ))) 106 106 ->appendChild( 107 107 id(new AphrontFormSubmitControl()) 108 - ->setValue('Submit Payment')); 108 + ->setValue('Add Payment Method')); 109 109 110 110 Javelin::initBehavior( 111 111 'stripe-payment-form',
+4
src/infrastructure/storage/patch/PhabricatorBuiltinPatchList.php
··· 1206 1206 'type' => 'sql', 1207 1207 'name' => $this->getPatchPath('20130322.phortune.sql'), 1208 1208 ), 1209 + '20130323.phortunepayment.sql' => array( 1210 + 'type' => 'sql', 1211 + 'name' => $this->getPatchPath('20130323.phortunepayment.sql'), 1212 + ), 1209 1213 ); 1210 1214 } 1211 1215
+11 -8
webroot/rsrc/js/application/phortune/behavior-stripe-payment-form.js
··· 3 3 * @requires javelin-behavior 4 4 * javelin-dom 5 5 * javelin-json 6 + * javelin-workflow 6 7 * stripe-core 7 8 */ 8 9 ··· 73 74 } 74 75 if (errors.length != 0) { 75 76 cardErrors.value = JX.JSON.stringify(errors); 76 - root.submit(); 77 - return true; 77 + 78 + JX.Workflow.newFromForm(root) 79 + .start(); 80 + 81 + return; 78 82 } 79 83 80 84 // no errors detected so contact Stripe asynchronously ··· 110 114 // success - we can use the token to create a customer object with 111 115 // Stripe and let the billing commence! 112 116 var token = response['id']; 117 + cardErrors.value = '[]'; 113 118 stripeToken.value = token; 114 119 } 115 - root.submit(); 120 + 121 + JX.Workflow.newFromForm(root) 122 + .start(); 116 123 } 117 124 118 - JX.DOM.listen( 119 - root, 120 - 'submit', 121 - null, 122 - onsubmit); 125 + JX.DOM.listen(root, 'submit', null, onsubmit); 123 126 });