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

Add initial cut of PayPal and pay-once-at-checkout providers to Phortune

Summary:
Paypal doesn't let us capture cards in a PCI-free way like Stripe and Balanced do, but we can provide a "pay with paypal" option at checkout. (For subscriptions, we'll have to invoice monthly to retain control over billing, but this doesn't seem wildly unreasonable.) The bitcoin provider MtGox works in a similar way, as do some other providers we might some day want to implement.

This adds:

- Hooks to providers so they can offer "pay once at checkout" workflows.
- Hooks so providers can have controllers, for redirect-based third-party workflows.
- Basic Paypal integration using the "Express Checkout Merchant API", which seems like the best fit for our use case. This only goes as far as shoving the user through the payment flow; we don't actually capture payments yet (paypal has around 35 different APIs, but this one seems to be the only PCI-free one which wouldn't give users an awful experience).

This diff is fairly checkpointey, but Phortune doesn't really bill anything yet anyway. Ref T2787.

Test Plan: Ran through Paypal sandbox workflow; "paid" for stuff.

Reviewers: btrahan

Reviewed By: btrahan

CC: aran

Maniphest Tasks: T2787

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

+369 -16
+4
src/__phutil_library_map__.php
··· 1604 1604 'PhortunePaymentMethodViewController' => 'applications/phortune/controller/PhortunePaymentMethodViewController.php', 1605 1605 'PhortunePaymentProvider' => 'applications/phortune/provider/PhortunePaymentProvider.php', 1606 1606 'PhortunePaymentProviderTestCase' => 'applications/phortune/provider/__tests__/PhortunePaymentProviderTestCase.php', 1607 + 'PhortunePaypalPaymentProvider' => 'applications/phortune/provider/PhortunePaypalPaymentProvider.php', 1607 1608 'PhortuneProduct' => 'applications/phortune/storage/PhortuneProduct.php', 1608 1609 'PhortuneProductEditController' => 'applications/phortune/controller/PhortuneProductEditController.php', 1609 1610 'PhortuneProductEditor' => 'applications/phortune/editor/PhortuneProductEditor.php', ··· 1612 1613 'PhortuneProductTransaction' => 'applications/phortune/storage/PhortuneProductTransaction.php', 1613 1614 'PhortuneProductTransactionQuery' => 'applications/phortune/query/PhortuneProductTransactionQuery.php', 1614 1615 'PhortuneProductViewController' => 'applications/phortune/controller/PhortuneProductViewController.php', 1616 + 'PhortuneProviderController' => 'applications/phortune/controller/PhortuneProviderController.php', 1615 1617 'PhortunePurchase' => 'applications/phortune/storage/PhortunePurchase.php', 1616 1618 'PhortuneStripePaymentProvider' => 'applications/phortune/provider/PhortuneStripePaymentProvider.php', 1617 1619 'PhortuneTestExtraPaymentProvider' => 'applications/phortune/provider/__tests__/PhortuneTestExtraPaymentProvider.php', ··· 3335 3337 'PhortunePaymentMethodQuery' => 'PhabricatorCursorPagedPolicyAwareQuery', 3336 3338 'PhortunePaymentMethodViewController' => 'PhabricatorController', 3337 3339 'PhortunePaymentProviderTestCase' => 'PhabricatorTestCase', 3340 + 'PhortunePaypalPaymentProvider' => 'PhortunePaymentProvider', 3338 3341 'PhortuneProduct' => 3339 3342 array( 3340 3343 0 => 'PhortuneDAO', ··· 3347 3350 'PhortuneProductTransaction' => 'PhabricatorApplicationTransaction', 3348 3351 'PhortuneProductTransactionQuery' => 'PhabricatorApplicationTransactionQuery', 3349 3352 'PhortuneProductViewController' => 'PhortuneController', 3353 + 'PhortuneProviderController' => 'PhortuneController', 3350 3354 'PhortunePurchase' => 'PhortuneDAO', 3351 3355 'PhortuneStripePaymentProvider' => 'PhortunePaymentProvider', 3352 3356 'PhortuneTestExtraPaymentProvider' => 'PhortunePaymentProvider',
+2
src/applications/phortune/application/PhabricatorApplicationPhortune.php
··· 49 49 'view/(?P<id>\d+)/' => 'PhortuneProductViewController', 50 50 'edit/(?:(?P<id>\d+)/)?' => 'PhortuneProductEditController', 51 51 ), 52 + 'provider/(?P<digest>[^/]+)/(?P<action>[^/]+)/' 53 + => 'PhortuneProviderController', 52 54 ), 53 55 ); 54 56 }
+47 -15
src/applications/phortune/controller/PhortuneAccountBuyController.php
··· 110 110 foreach ($methods as $method) { 111 111 $method_control->addButton( 112 112 $method->getID(), 113 - $method->getName(), 113 + $method->getBrand().' / '.$method->getLastFourDigits(), 114 114 $method->getDescription()); 115 115 } 116 116 } ··· 118 118 $payment_method_uri = $this->getApplicationURI( 119 119 $account->getID().'/paymentmethod/edit/'); 120 120 121 - $new_method = phutil_tag( 122 - 'a', 123 - array( 124 - 'href' => $payment_method_uri, 125 - 'sigil' => 'workflow', 126 - ), 127 - pht('Add New Payment Method')); 128 - 129 121 $form = id(new AphrontFormView()) 130 122 ->setUser($user) 131 - ->appendChild($method_control) 132 - ->appendChild( 123 + ->appendChild($method_control); 124 + 125 + $add_providers = PhortunePaymentProvider::getProvidersForAddPaymentMethod(); 126 + if ($add_providers) { 127 + $new_method = phutil_tag( 128 + 'a', 129 + array( 130 + 'class' => 'button grey', 131 + 'href' => $payment_method_uri, 132 + 'sigil' => 'workflow', 133 + ), 134 + pht('Add New Payment Method')); 135 + $form->appendChild( 133 136 id(new AphrontFormMarkupControl()) 134 - ->setValue($new_method)) 135 - ->appendChild( 136 - id(new AphrontFormSubmitControl()) 137 - ->setValue(pht("Dolla Dolla Bill Y'all"))); 137 + ->setValue($new_method)); 138 + } 139 + 140 + if ($methods || $add_providers) { 141 + $form 142 + ->appendChild( 143 + id(new AphrontFormSubmitControl()) 144 + ->setValue(pht("Submit Payment")) 145 + ->setDisabled(!$methods)); 146 + } 147 + 148 + $provider_form = null; 149 + 150 + $pay_providers = PhortunePaymentProvider::getProvidersForOneTimePayment(); 151 + if ($pay_providers) { 152 + $one_time_options = array(); 153 + foreach ($pay_providers as $provider) { 154 + $one_time_options[] = $provider->renderOneTimePaymentButton( 155 + $account, 156 + $cart, 157 + $user); 158 + } 159 + 160 + $provider_form = id(new AphrontFormLayoutView()) 161 + ->setPadded(true) 162 + ->setBackgroundShading(true); 163 + $provider_form->appendChild( 164 + id(new AphrontFormMarkupControl()) 165 + ->setLabel('Pay With') 166 + ->setValue($one_time_options)); 167 + } 138 168 139 169 return $this->buildApplicationPage( 140 170 array( 141 171 $panel, 142 172 $form, 173 + phutil_tag('br', array()), 174 + $provider_form, 143 175 ), 144 176 array( 145 177 'title' => $title,
+64
src/applications/phortune/controller/PhortuneProviderController.php
··· 1 + <?php 2 + 3 + final class PhortuneProviderController extends PhortuneController { 4 + 5 + private $digest; 6 + private $action; 7 + 8 + public function willProcessRequest(array $data) { 9 + $this->digest = $data['digest']; 10 + $this->setAction($data['action']); 11 + } 12 + 13 + public function setAction($action) { 14 + $this->action = $action; 15 + return $this; 16 + } 17 + 18 + public function getAction() { 19 + return $this->action; 20 + } 21 + 22 + public function processRequest() { 23 + $request = $this->getRequest(); 24 + $user = $request->getUser(); 25 + 26 + 27 + // NOTE: This use of digests to identify payment providers is because 28 + // payment provider keys don't necessarily have restrictions on what they 29 + // contain (so they might have stuff that's not safe to put in URIs), and 30 + // using digests prevents errors with URI encoding. 31 + 32 + $provider = PhortunePaymentProvider::getProviderByDigest($this->digest); 33 + if (!$provider) { 34 + throw new Exception("Invalid payment provider digest!"); 35 + } 36 + 37 + if (!$provider->canRespondToControllerAction($this->getAction())) { 38 + return new Aphront404Response(); 39 + } 40 + 41 + 42 + $response = $provider->processControllerRequest($this, $request); 43 + 44 + if ($response instanceof AphrontResponse) { 45 + return $response; 46 + } 47 + 48 + $title = 'Phortune'; 49 + 50 + return $this->buildApplicationPage( 51 + $response, 52 + array( 53 + 'title' => $title, 54 + 'device' => true, 55 + 'dust' => true, 56 + )); 57 + } 58 + 59 + 60 + public function loadCart($id) { 61 + return id(new PhortuneCart()); 62 + } 63 + 64 + }
+12 -1
src/applications/phortune/option/PhabricatorPhortuneConfigOptions.php
··· 38 38 "NOTE: Enabling this provider gives all users infinite free ". 39 39 "money! You should enable it **ONLY** for testing and ". 40 40 "development.")) 41 + ->setLocked(true), 42 + $this->newOption('phortune.paypal.api-username', 'string', null) 41 43 ->setLocked(true) 42 - 44 + ->setDescription( 45 + pht('PayPal API username.')), 46 + $this->newOption('phortune.paypal.api-password', 'string', null) 47 + ->setHidden(true) 48 + ->setDescription( 49 + pht('PayPal API password.')), 50 + $this->newOption('phortune.paypal.api-signature', 'string', null) 51 + ->setHidden(true) 52 + ->setDescription( 53 + pht('PayPal API signature.')), 43 54 ); 44 55 } 45 56
+64
src/applications/phortune/provider/PhortunePaymentProvider.php
··· 37 37 return $providers; 38 38 } 39 39 40 + public static function getProvidersForOneTimePayment() { 41 + $providers = self::getEnabledProviders(); 42 + foreach ($providers as $key => $provider) { 43 + if (!$provider->canProcessOneTimePayments()) { 44 + unset($providers[$key]); 45 + } 46 + } 47 + return $providers; 48 + } 49 + 50 + public static function getProviderByDigest($digest) { 51 + $providers = self::getEnabledProviders(); 52 + foreach ($providers as $key => $provider) { 53 + $provider_digest = PhabricatorHash::digestForIndex($key); 54 + if ($provider_digest == $digest) { 55 + return $provider; 56 + } 57 + } 58 + return null; 59 + } 60 + 40 61 abstract public function isEnabled(); 41 62 42 63 final public function getProviderKey() { ··· 136 157 throw new PhortuneNotImplementedException($this); 137 158 } 138 159 160 + 161 + /* -( One-Time Payments )-------------------------------------------------- */ 162 + 163 + 164 + public function canProcessOneTimePayments() { 165 + return false; 166 + } 167 + 168 + public function renderOneTimePaymentButton( 169 + PhortuneAccount $account, 170 + PhortuneCart $cart, 171 + PhabricatorUser $user) { 172 + throw new PhortuneNotImplementedException($this); 173 + } 174 + 175 + 176 + /* -( Controllers )-------------------------------------------------------- */ 177 + 178 + 179 + final public function getControllerURI( 180 + $action, 181 + array $params = array()) { 182 + 183 + $digest = PhabricatorHash::digestForIndex($this->getProviderKey()); 184 + 185 + $app = PhabricatorApplication::getByClass('PhabricatorApplicationPhortune'); 186 + $path = $app->getBaseURI().'provider/'.$digest.'/'.$action.'/'; 187 + 188 + $uri = new PhutilURI($path); 189 + $uri->setQueryParams($params); 190 + 191 + return PhabricatorEnv::getURI((string)$uri); 192 + } 193 + 194 + public function canRespondToControllerAction($action) { 195 + return false; 196 + } 197 + 198 + public function processControllerRequest( 199 + PhortuneProviderController $controller, 200 + AphrontRequest $request) { 201 + throw new PhortuneNotImplementedException($this); 202 + } 139 203 140 204 }
+168
src/applications/phortune/provider/PhortunePaypalPaymentProvider.php
··· 1 + <?php 2 + 3 + final class PhortunePaypalPaymentProvider extends PhortunePaymentProvider { 4 + 5 + public function isEnabled() { 6 + return $this->getPaypalAPIUsername() && 7 + $this->getPaypalAPIPassword() && 8 + $this->getPaypalAPISignature(); 9 + } 10 + 11 + public function getProviderType() { 12 + return 'paypal'; 13 + } 14 + 15 + public function getProviderDomain() { 16 + return 'paypal.com'; 17 + } 18 + 19 + public function getPaymentMethodDescription() { 20 + return 'Paypal Account'; 21 + } 22 + 23 + public function getPaymentMethodIcon() { 24 + return 'rsrc/phortune/paypal.png'; 25 + } 26 + 27 + public function getPaymentMethodProviderDescription() { 28 + return "Paypal"; 29 + } 30 + 31 + 32 + public function canHandlePaymentMethod(PhortunePaymentMethod $method) { 33 + $type = $method->getMetadataValue('type'); 34 + return ($type == 'paypal'); 35 + } 36 + 37 + protected function executeCharge( 38 + PhortunePaymentMethod $payment_method, 39 + PhortuneCharge $charge) { 40 + throw new Exception("!"); 41 + } 42 + 43 + private function getPaypalAPIUsername() { 44 + return PhabricatorEnv::getEnvConfig('phortune.paypal.api-username'); 45 + } 46 + 47 + private function getPaypalAPIPassword() { 48 + return PhabricatorEnv::getEnvConfig('phortune.paypal.api-password'); 49 + } 50 + 51 + private function getPaypalAPISignature() { 52 + return PhabricatorEnv::getEnvConfig('phortune.paypal.api-signature'); 53 + } 54 + 55 + /* -( One-Time Payments )-------------------------------------------------- */ 56 + 57 + public function canProcessOneTimePayments() { 58 + return true; 59 + } 60 + 61 + public function renderOneTimePaymentButton( 62 + PhortuneAccount $account, 63 + PhortuneCart $cart, 64 + PhabricatorUser $user) { 65 + 66 + $uri = $this->getControllerURI( 67 + 'checkout', 68 + array( 69 + 'cartID' => $cart->getID(), 70 + )); 71 + 72 + return phabricator_form( 73 + $user, 74 + array( 75 + 'action' => $uri, 76 + 'method' => 'POST', 77 + ), 78 + phutil_tag( 79 + 'button', 80 + array( 81 + 'class' => 'green', 82 + 'type' => 'submit', 83 + ), 84 + pht('Pay with Paypal'))); 85 + } 86 + 87 + 88 + /* -( Controllers )-------------------------------------------------------- */ 89 + 90 + 91 + public function canRespondToControllerAction($action) { 92 + switch ($action) { 93 + case 'checkout': 94 + case 'charge': 95 + case 'cancel': 96 + return true; 97 + } 98 + return parent::canRespondToControllerAction(); 99 + } 100 + 101 + public function processControllerRequest( 102 + PhortuneProviderController $controller, 103 + AphrontRequest $request) { 104 + 105 + $cart = $controller->loadCart($request->getInt('cartID')); 106 + if (!$cart) { 107 + return new Aphront404Response(); 108 + } 109 + 110 + switch ($controller->getAction()) { 111 + case 'checkout': 112 + $return_uri = $this->getControllerURI( 113 + 'charge', 114 + array( 115 + 'cartID' => $cart->getID(), 116 + )); 117 + 118 + $cancel_uri = $this->getControllerURI( 119 + 'cancel', 120 + array( 121 + 'cartID' => $cart->getID(), 122 + )); 123 + 124 + $total_in_cents = $cart->getTotalInCents(); 125 + $price = PhortuneUtil::formatBareCurrency($total_in_cents); 126 + 127 + $result = $this 128 + ->newPaypalAPICall() 129 + ->setRawPayPalQuery( 130 + 'SetExpressCheckout', 131 + array( 132 + 'PAYMENTREQUEST_0_AMT' => $price, 133 + 'PAYMENTREQUEST_0_CURRENCYCODE' => 'USD', 134 + 'RETURNURL' => $return_uri, 135 + 'CANCELURL' => $cancel_uri, 136 + 'PAYMENTREQUEST_0_PAYMENTACTION' => 'Sale', 137 + )) 138 + ->resolve(); 139 + 140 + $uri = new PhutilURI('https://www.sandbox.paypal.com/cgi-bin/webscr'); 141 + $uri->setQueryParams( 142 + array( 143 + 'cmd' => '_express-checkout', 144 + 'token' => $result['TOKEN'], 145 + )); 146 + 147 + return id(new AphrontRedirectResponse())->setURI($uri); 148 + case 'charge': 149 + var_dump($_REQUEST); 150 + break; 151 + case 'cancel': 152 + var_dump($_REQUEST); 153 + break; 154 + } 155 + 156 + throw new Exception("The rest of this isn't implemented yet."); 157 + } 158 + 159 + private function newPaypalAPICall() { 160 + return id(new PhutilPayPalAPIFuture()) 161 + ->setHost('https://api-3t.sandbox.paypal.com/nvp') 162 + ->setAPIUsername($this->getPaypalAPIUsername()) 163 + ->setAPIPassword($this->getPaypalAPIPassword()) 164 + ->setAPISignature($this->getPaypalAPISignature()); 165 + } 166 + 167 + 168 + }
+4
src/applications/phortune/storage/PhortuneCart.php
··· 28 28 return $this; 29 29 } 30 30 31 + public function getTotalInCents() { 32 + return 123; 33 + } 34 + 31 35 public function getPurchases() { 32 36 if ($this->purchases === null) { 33 37 throw new Exception("Purchases not attached to cart!");
+4
src/applications/phortune/util/PhortuneUtil.php
··· 31 31 return $display_value; 32 32 } 33 33 34 + public static function formatBareCurrency($price_in_cents) { 35 + return str_replace('$', '', self::formatCurrency($price_in_cents)); 36 + } 37 + 34 38 }