@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.

Implement Balanced Payments as a PhortunePaymentProvider

Summary:
Allows Balanced payment methods to be added. This works essentially the same way as Stripe, except everything is a little bit different.

Slightly more stuff could be shared, but I feel //mostly// good about this. I'll probably do a bit more cleanup next. Some of the error handling is messy, in particular.

Ref T2787.

Test Plan: Added Balanced and Stripe payment methods.

Reviewers: btrahan, chad

Reviewed By: btrahan

CC: aran

Maniphest Tasks: T2787

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

+308 -35
+15 -1
src/__celerity_resource_map__.php
··· 1274 1274 ), 1275 1275 'disk' => '/rsrc/js/application/diffusion/behavior-audit-preview.js', 1276 1276 ), 1277 + 'javelin-behavior-balanced-payment-form' => 1278 + array( 1279 + 'uri' => '/res/2a850a31/rsrc/js/application/phortune/behavior-balanced-payment-form.js', 1280 + 'type' => 'js', 1281 + 'requires' => 1282 + array( 1283 + 0 => 'javelin-behavior', 1284 + 1 => 'javelin-dom', 1285 + 2 => 'javelin-json', 1286 + 3 => 'javelin-workflow', 1287 + 4 => 'phortune-credit-card-form', 1288 + ), 1289 + 'disk' => '/rsrc/js/application/phortune/behavior-balanced-payment-form.js', 1290 + ), 1277 1291 'javelin-behavior-conpherence-drag-and-drop-photo' => 1278 1292 array( 1279 1293 'uri' => '/res/9e3eb1cd/rsrc/js/application/conpherence/behavior-drag-and-drop-photo.js', ··· 2258 2272 ), 2259 2273 'javelin-behavior-stripe-payment-form' => 2260 2274 array( 2261 - 'uri' => '/res/62dc91b4/rsrc/js/application/phortune/behavior-stripe-payment-form.js', 2275 + 'uri' => '/res/2ae12d96/rsrc/js/application/phortune/behavior-stripe-payment-form.js', 2262 2276 'type' => 'js', 2263 2277 'requires' => 2264 2278 array(
+4 -2
src/__phutil_library_map__.php
··· 1219 1219 'PhabricatorPhabricatorOAuthConfigOptions' => 'applications/config/option/PhabricatorPhabricatorOAuthConfigOptions.php', 1220 1220 'PhabricatorPhameConfigOptions' => 'applications/phame/config/PhabricatorPhameConfigOptions.php', 1221 1221 'PhabricatorPholioConfigOptions' => 'applications/pholio/config/PhabricatorPholioConfigOptions.php', 1222 + 'PhabricatorPhortuneConfigOptions' => 'applications/phortune/option/PhabricatorPhortuneConfigOptions.php', 1222 1223 'PhabricatorPhrequentConfigOptions' => 'applications/phrequent/config/PhabricatorPhrequentConfigOptions.php', 1223 1224 'PhabricatorPhrictionConfigOptions' => 'applications/phriction/config/PhabricatorPhrictionConfigOptions.php', 1224 1225 'PhabricatorPinboardItemView' => 'view/layout/PhabricatorPinboardItemView.php', ··· 1400 1401 'PhabricatorStorageManagementUpgradeWorkflow' => 'infrastructure/storage/management/workflow/PhabricatorStorageManagementUpgradeWorkflow.php', 1401 1402 'PhabricatorStorageManagementWorkflow' => 'infrastructure/storage/management/workflow/PhabricatorStorageManagementWorkflow.php', 1402 1403 'PhabricatorStoragePatch' => 'infrastructure/storage/management/PhabricatorStoragePatch.php', 1403 - 'PhabricatorStripeConfigOptions' => 'applications/phortune/option/PhabricatorStripeConfigOptions.php', 1404 1404 'PhabricatorSubscribableInterface' => 'applications/subscriptions/interface/PhabricatorSubscribableInterface.php', 1405 1405 'PhabricatorSubscribersQuery' => 'applications/subscriptions/query/PhabricatorSubscribersQuery.php', 1406 1406 'PhabricatorSubscriptionsEditController' => 'applications/subscriptions/controller/PhabricatorSubscriptionsEditController.php', ··· 1580 1580 'PhortuneAccountTransaction' => 'applications/phortune/storage/PhortuneAccountTransaction.php', 1581 1581 'PhortuneAccountTransactionQuery' => 'applications/phortune/query/PhortuneAccountTransactionQuery.php', 1582 1582 'PhortuneAccountViewController' => 'applications/phortune/controller/PhortuneAccountViewController.php', 1583 + 'PhortuneBalancedPaymentProvider' => 'applications/phortune/provider/PhortuneBalancedPaymentProvider.php', 1583 1584 'PhortuneCart' => 'applications/phortune/storage/PhortuneCart.php', 1584 1585 'PhortuneCharge' => 'applications/phortune/storage/PhortuneCharge.php', 1585 1586 'PhortuneController' => 'applications/phortune/controller/PhortuneController.php', ··· 2923 2924 'PhabricatorPhabricatorOAuthConfigOptions' => 'PhabricatorApplicationConfigOptions', 2924 2925 'PhabricatorPhameConfigOptions' => 'PhabricatorApplicationConfigOptions', 2925 2926 'PhabricatorPholioConfigOptions' => 'PhabricatorApplicationConfigOptions', 2927 + 'PhabricatorPhortuneConfigOptions' => 'PhabricatorApplicationConfigOptions', 2926 2928 'PhabricatorPhrequentConfigOptions' => 'PhabricatorApplicationConfigOptions', 2927 2929 'PhabricatorPhrictionConfigOptions' => 'PhabricatorApplicationConfigOptions', 2928 2930 'PhabricatorPinboardItemView' => 'AphrontView', ··· 3091 3093 'PhabricatorStorageManagementStatusWorkflow' => 'PhabricatorStorageManagementWorkflow', 3092 3094 'PhabricatorStorageManagementUpgradeWorkflow' => 'PhabricatorStorageManagementWorkflow', 3093 3095 'PhabricatorStorageManagementWorkflow' => 'PhutilArgumentWorkflow', 3094 - 'PhabricatorStripeConfigOptions' => 'PhabricatorApplicationConfigOptions', 3095 3096 'PhabricatorSubscribersQuery' => 'PhabricatorQuery', 3096 3097 'PhabricatorSubscriptionsEditController' => 'PhabricatorController', 3097 3098 'PhabricatorSubscriptionsEditor' => 'PhabricatorEditor', ··· 3302 3303 'PhortuneAccountTransaction' => 'PhabricatorApplicationTransaction', 3303 3304 'PhortuneAccountTransactionQuery' => 'PhabricatorApplicationTransactionQuery', 3304 3305 'PhortuneAccountViewController' => 'PhortuneController', 3306 + 'PhortuneBalancedPaymentProvider' => 'PhortunePaymentProvider', 3305 3307 'PhortuneCart' => 'PhortuneDAO', 3306 3308 'PhortuneCharge' => 'PhortuneDAO', 3307 3309 'PhortuneController' => 'PhabricatorController',
+1 -1
src/applications/phortune/exception/PhortuneNotImplementedException.php
··· 5 5 public function __construct(PhortunePaymentProvider $provider) { 6 6 $class = get_class($provider); 7 7 return parent::__construct( 8 - "Provider '{$provider}' does not implement this method."); 8 + "Provider '{$class}' does not implement this method."); 9 9 } 10 10 11 11 }
+31
src/applications/phortune/option/PhabricatorPhortuneConfigOptions.php
··· 1 + <?php 2 + 3 + final class PhabricatorPhortuneConfigOptions 4 + extends PhabricatorApplicationConfigOptions { 5 + 6 + public function getName() { 7 + return pht("Phortune"); 8 + } 9 + 10 + public function getDescription() { 11 + return pht("Configure payments and billing."); 12 + } 13 + 14 + public function getOptions() { 15 + return array( 16 + $this->newOption('phortune.stripe.publishable-key', 'string', null) 17 + ->setLocked(true) 18 + ->setDescription(pht('Stripe publishable key.')), 19 + $this->newOption('phortune.stripe.secret-key', 'string', null) 20 + ->setHidden(true) 21 + ->setDescription(pht('Stripe secret key.')), 22 + $this->newOption('phortune.balanced.marketplace-uri', 'string', null) 23 + ->setLocked(true) 24 + ->setDescription(pht('Balanced Marketplace URI.')), 25 + $this->newOption('phortune.balanced.secret-key', 'string', null) 26 + ->setHidden(true) 27 + ->setDescription(pht('Balanced secret key.')), 28 + ); 29 + } 30 + 31 + }
-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 - }
+171
src/applications/phortune/provider/PhortuneBalancedPaymentProvider.php
··· 1 + <?php 2 + 3 + final class PhortuneBalancedPaymentProvider extends PhortunePaymentProvider { 4 + 5 + public function isEnabled() { 6 + return $this->getMarketplaceURI() && 7 + $this->getSecretKey(); 8 + } 9 + 10 + public function getProviderType() { 11 + return 'balanced'; 12 + } 13 + 14 + public function getProviderDomain() { 15 + return 'balancedpayments.com'; 16 + } 17 + 18 + public function getPaymentMethodDescription() { 19 + return pht('Add Credit or Debit Card'); 20 + } 21 + 22 + public function getPaymentMethodIcon() { 23 + return celerity_get_resource_uri('/rsrc/image/phortune/balanced.png'); 24 + } 25 + 26 + public function getPaymentMethodProviderDescription() { 27 + return pht('Processed by Balanced'); 28 + } 29 + 30 + 31 + public function canHandlePaymentMethod(PhortunePaymentMethod $method) { 32 + $type = $method->getMetadataValue('type'); 33 + return ($type === 'balanced.account'); 34 + } 35 + 36 + protected function executeCharge( 37 + PhortunePaymentMethod $method, 38 + PhortuneCharge $charge) { 39 + throw new PhortuneNotImplementedException($this); 40 + } 41 + 42 + private function getMarketplaceURI() { 43 + return PhabricatorEnv::getEnvConfig('phortune.balanced.marketplace-uri'); 44 + } 45 + 46 + private function getSecretKey() { 47 + return PhabricatorEnv::getEnvConfig('phortune.balanced.secret-key'); 48 + } 49 + 50 + 51 + /* -( Adding Payment Methods )--------------------------------------------- */ 52 + 53 + 54 + public function canCreatePaymentMethods() { 55 + return true; 56 + } 57 + 58 + 59 + /** 60 + * @phutil-external-symbol class Balanced\Settings 61 + * @phutil-external-symbol class Balanced\Marketplace 62 + * @phutil-external-symbol class RESTful\Exceptions\HTTPError 63 + */ 64 + public function createPaymentMethodFromRequest( 65 + AphrontRequest $request, 66 + PhortunePaymentMethod $method) { 67 + 68 + $card_errors = $request->getStr('cardErrors'); 69 + $balanced_data = $request->getStr('balancedCardData'); 70 + 71 + $errors = array(); 72 + if ($card_errors) { 73 + $raw_errors = json_decode($card_errors); 74 + $errors = $this->parseRawCreatePaymentMethodErrors($raw_errors); 75 + } 76 + 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; 93 + 94 + try { 95 + 96 + Balanced\Settings::$api_key = $this->getSecretKey(); 97 + $buyer = Balanced\Marketplace::mine()->createBuyer( 98 + null, 99 + $data['uri'], 100 + array( 101 + 'description' => $description, 102 + )); 103 + 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 + } 109 + 110 + $exp_string = $data['expiration_year'].'-'.$data['expiration_month']; 111 + $epoch = strtotime($exp_string); 112 + 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 + )); 122 + } 123 + 124 + return $errors; 125 + } 126 + 127 + public function renderCreatePaymentMethodForm( 128 + AphrontRequest $request, 129 + array $errors) { 130 + 131 + $ccform = id(new PhortuneCreditCardForm()) 132 + ->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) 136 + ->addScript('https://js.balancedpayments.com/v1/balanced.js'); 137 + 138 + Javelin::initBehavior( 139 + 'balanced-payment-form', 140 + array( 141 + 'balancedMarketplaceURI' => $this->getMarketplaceURI(), 142 + 'formID' => $ccform->getFormID(), 143 + )); 144 + 145 + return $ccform->buildForm(); 146 + } 147 + 148 + private function parseRawCreatePaymentMethodErrors(array $raw_errors) { 149 + $errors = array(); 150 + 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; 165 + } 166 + } 167 + 168 + return $errors; 169 + } 170 + 171 + }
+9 -5
src/applications/phortune/provider/PhortuneStripePaymentProvider.php
··· 59 59 } 60 60 61 61 private function getPublishableKey() { 62 - return PhabricatorEnv::getEnvConfig('stripe.publishable-key'); 62 + return PhabricatorEnv::getEnvConfig('phortune.stripe.publishable-key'); 63 63 } 64 64 65 65 private function getSecretKey() { 66 - return PhabricatorEnv::getEnvConfig('stripe.secret-key'); 66 + return PhabricatorEnv::getEnvConfig('phortune.stripe.secret-key'); 67 67 } 68 68 69 69 ··· 90 90 if ($card_errors) { 91 91 $raw_errors = json_decode($card_errors); 92 92 $errors = $this->parseRawCreatePaymentMethodErrors($raw_errors); 93 - } else if (!$stripe_token) { 94 - $errors[] = pht('There was an unknown error processing your card.'); 95 93 } 96 94 97 - $secret_key = $this->getSecretKey(); 95 + if (!$errors) { 96 + if (!$stripe_token) { 97 + $errors[] = pht('There was an unknown error processing your card.'); 98 + } 99 + } 98 100 99 101 if (!$errors) { 100 102 $root = dirname(phutil_get_library_root('phabricator')); ··· 102 104 103 105 try { 104 106 // First, make sure the token is valid. 107 + $secret_key = $this->getSecretKey(); 108 + 105 109 $info = id(new Stripe_Token())->retrieve($stripe_token, $secret_key); 106 110 107 111 $account_phid = $method->getAccountPHID();
+71
webroot/rsrc/js/application/phortune/behavior-balanced-payment-form.js
··· 1 + /** 2 + * @provides javelin-behavior-balanced-payment-form 3 + * @requires javelin-behavior 4 + * javelin-dom 5 + * javelin-json 6 + * javelin-workflow 7 + * phortune-credit-card-form 8 + */ 9 + 10 + JX.behavior('balanced-payment-form', function(config) { 11 + balanced.init(config.balancedMarketplaceURI); 12 + 13 + var root = JX.$(config.formID); 14 + var ccform = new JX.PhortuneCreditCardForm(root); 15 + 16 + var onsubmit = function(e) { 17 + e.kill(); 18 + 19 + var cardData = ccform.getCardData(); 20 + var errors = []; 21 + 22 + if (!balanced.card.isCardNumberValid(cardData.number)) { 23 + errors.push('number'); 24 + } 25 + 26 + if (!balanced.card.isSecurityCodeValid(cardData.number, cardData.cvc)) { 27 + errors.push('cvc'); 28 + } 29 + 30 + if (!balanced.card.isExpiryValid(cardData.month, cardData.year)) { 31 + errors.push('expiry'); 32 + } 33 + 34 + if (errors.length) { 35 + JX.Workflow 36 + .newFromForm(root, {cardErrors: JX.JSON.stringify(errors)}) 37 + .start(); 38 + return; 39 + } 40 + 41 + var data = { 42 + card_number: cardData.number, 43 + security_code: cardData.cvc, 44 + expiration_month: cardData.month, 45 + expiration_year: cardData.year 46 + }; 47 + 48 + balanced.card.create(data, onresponse); 49 + } 50 + 51 + var onresponse = function(response) { 52 + 53 + var errors = []; 54 + if (response.error) { 55 + errors = [response.error.type]; 56 + } else if (response.status != 201) { 57 + errors = ['balanced:' + response.status]; 58 + } 59 + 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(); 68 + } 69 + 70 + JX.DOM.listen(root, 'submit', null, onsubmit); 71 + });
+6 -1
webroot/rsrc/js/application/phortune/behavior-stripe-payment-form.js
··· 59 59 token = response.id; 60 60 } 61 61 62 + var params = { 63 + cardErrors: JX.JSON.stringify(errors), 64 + stripeToken: token 65 + }; 66 + 62 67 JX.Workflow 63 - .newFromForm(root, {cardErrors: errors, stripeToken: token}) 68 + .newFromForm(root, params) 64 69 .start(); 65 70 } 66 71