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

Give WePay complete payment logic in Phortune

Summary:
Ref T2787. This doesn't get all the edge cases quite correct, but is generally a safe, complete payment workflow:

- Shares the actual charging state logic.
- Makes it appropriately stateful with locking and transactions.
- Gets the main flow correct.
- Detects failure cases, just tends to blow up rather than help the user resolve them.

Test Plan:
- Charged with WePay.
- Charged with Infinite Free Money.
- Resumed an abandoned cart.
- Hit all failure states where we just dead-end the cart. Not ideal, but (seemingly) complete/safe/correct.

Reviewers: btrahan

Reviewed By: btrahan

Subscribers: epriestley

Maniphest Tasks: T2787

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

+143 -59
+20 -17
src/applications/phortune/controller/PhortuneCartCheckoutController.php
··· 38 38 // This is the expected, normal state for a cart that's ready for 39 39 // checkout. 40 40 break; 41 + case PhortuneCart::STATUS_PURCHASING: 42 + // We've started the purchase workflow for this cart, but were not able 43 + // to complete it. If the workflow is on an external site, this could 44 + // happen because the user abandoned the workflow. Just return them to 45 + // the right place so they can resume where they left off. 46 + $uri = $cart->getMetadataValue('provider.checkoutURI'); 47 + if ($uri !== null) { 48 + return id(new AphrontRedirectResponse()) 49 + ->setIsExternal(true) 50 + ->setURI($uri); 51 + } 52 + 53 + return $this->newDialog() 54 + ->setTitle(pht('Charge Failed')) 55 + ->appendParagraph( 56 + pht( 57 + 'Failed to charge this cart.')) 58 + ->addCancelButton($cancel_uri); 59 + break; 41 60 case PhortuneCart::STATUS_CHARGED: 42 61 // TODO: This is really bad (we took your money and at least partially 43 62 // failed to fulfill your order) and should have better steps forward. ··· 89 108 if (!$errors) { 90 109 $provider = $method->buildPaymentProvider(); 91 110 92 - $charge = id(new PhortuneCharge()) 93 - ->setAccountPHID($account->getPHID()) 94 - ->setCartPHID($cart->getPHID()) 95 - ->setAuthorPHID($viewer->getPHID()) 96 - ->setPaymentProviderKey($provider->getProviderKey()) 97 - ->setPaymentMethodPHID($method->getPHID()) 98 - ->setAmountAsCurrency($cart->getTotalPriceAsCurrency()) 99 - ->setStatus(PhortuneCharge::STATUS_PENDING); 100 - 101 - $charge->openTransaction(); 102 - $charge->save(); 103 - 104 - $cart->setStatus(PhortuneCart::STATUS_PURCHASING); 105 - $cart->save(); 106 - $charge->saveTransaction(); 107 - 111 + $charge = $cart->willApplyCharge($viewer, $provider, $method); 108 112 $provider->applyCharge($method, $charge); 109 - 110 113 $cart->didApplyCharge($charge); 111 114 112 115 $done_uri = $cart->getDoneURI();
+10 -3
src/applications/phortune/provider/PhortunePaymentProvider.php
··· 204 204 ->setText($description) 205 205 ->setSubtext($details); 206 206 207 + // NOTE: We generate a local URI to make sure the form picks up CSRF tokens. 207 208 $uri = $this->getControllerURI( 208 209 'checkout', 209 210 array( 210 211 'cartID' => $cart->getID(), 211 - )); 212 + ), 213 + $local = true); 212 214 213 215 return phabricator_form( 214 216 $user, ··· 225 227 226 228 final public function getControllerURI( 227 229 $action, 228 - array $params = array()) { 230 + array $params = array(), 231 + $local = false) { 229 232 230 233 $digest = PhabricatorHash::digestForIndex($this->getProviderKey()); 231 234 ··· 235 238 $uri = new PhutilURI($path); 236 239 $uri->setQueryParams($params); 237 240 238 - return PhabricatorEnv::getURI((string)$uri); 241 + if ($local) { 242 + return $uri; 243 + } else { 244 + return PhabricatorEnv::getURI((string)$uri); 245 + } 239 246 } 240 247 241 248 public function canRespondToControllerAction($action) {
+36 -28
src/applications/phortune/provider/PhortuneWePayPaymentProvider.php
··· 91 91 return new Aphront404Response(); 92 92 } 93 93 94 - $cart_uri = '/phortune/cart/'.$cart->getID().'/'; 95 - 96 94 $root = dirname(phutil_get_library_root('phabricator')); 97 95 require_once $root.'/externals/wepay/wepay.php'; 98 96 ··· 101 99 $this->getWePayClientSecret()); 102 100 103 101 $wepay = new WePay($this->getWePayAccessToken()); 102 + 103 + $charge = id(new PhortuneChargeQuery()) 104 + ->setViewer($viewer) 105 + ->withCartPHIDs(array($cart->getPHID())) 106 + ->withStatuses( 107 + array( 108 + PhortuneCharge::STATUS_CHARGING, 109 + )) 110 + ->executeOne(); 111 + 112 + switch ($controller->getAction()) { 113 + case 'checkout': 114 + if ($charge) { 115 + throw new Exception(pht('Cart is already charging!')); 116 + } 117 + break; 118 + case 'charge': 119 + case 'cancel': 120 + if (!$charge) { 121 + throw new Exception(pht('Cart is not charging yet!')); 122 + } 123 + break; 124 + } 104 125 105 126 switch ($controller->getAction()) { 106 127 case 'checkout': ··· 142 163 'funding_sources' => 'bank,cc' 143 164 ); 144 165 166 + $cart->willApplyCharge($viewer, $this); 167 + 145 168 $result = $wepay->request('checkout/create', $params); 146 169 147 - // TODO: We must store "$result->checkout_id" on the Cart since the 148 - // user might not end up back here. Really this needs a bunch of junk. 170 + $cart->setMetadataValue('provider.checkoutURI', $result->checkout_uri); 171 + $cart->setMetadataValue('wepay.checkoutID', $result->checkout_id); 172 + $cart->save(); 149 173 150 174 $uri = new PhutilURI($result->checkout_uri); 151 175 return id(new AphrontRedirectResponse()) ··· 175 199 $result->state)); 176 200 } 177 201 178 - $currency = PhortuneCurrency::newFromString($checkout->gross, 'USD'); 179 - 180 202 $unguarded = AphrontWriteGuard::beginScopedUnguardedWrites(); 181 - 182 - $charge = id(new PhortuneCharge()) 183 - ->setAmountAsCurrency($currency) 184 - ->setAccountPHID($cart->getAccount()->getPHID()) 185 - ->setAuthorPHID($viewer->getPHID()) 186 - ->setPaymentProviderKey($this->getProviderKey()) 187 - ->setCartPHID($cart->getPHID()) 188 - ->setStatus(PhortuneCharge::STATUS_CHARGING) 189 - ->save(); 190 - 191 - $cart->openTransaction(); 192 - $charge->setStatus(PhortuneCharge::STATUS_CHARGED); 193 - $charge->save(); 194 - 195 - $cart->setStatus(PhortuneCart::STATUS_PURCHASED); 196 - $cart->save(); 197 - $cart->saveTransaction(); 198 - 203 + $cart->didApplyCharge($charge); 199 204 unset($unguarded); 200 205 201 206 return id(new AphrontRedirectResponse()) 202 - ->setIsExternal(true) 203 - ->setURI($cart_uri); 207 + ->setURI($cart->getDoneURI()); 204 208 case 'cancel': 205 - var_dump($_REQUEST); 209 + // TODO: I don't know how it's possible to cancel out of a WePay 210 + // charge workflow. 211 + throw new Exception( 212 + pht('How did you get here? WePay has no cancel flow in its UI...?')); 206 213 break; 207 214 } 208 215 209 - throw new Exception("The rest of this isn't implemented yet."); 216 + throw new Exception( 217 + pht('Unsupported action "%s".', $controller->getAction())); 210 218 } 211 219 212 220
+13
src/applications/phortune/query/PhortuneChargeQuery.php
··· 7 7 private $phids; 8 8 private $accountPHIDs; 9 9 private $cartPHIDs; 10 + private $statuses; 10 11 11 12 private $needCarts; 12 13 ··· 27 28 28 29 public function withCartPHIDs(array $cart_phids) { 29 30 $this->cartPHIDs = $cart_phids; 31 + return $this; 32 + } 33 + 34 + public function withStatuses(array $statuses) { 35 + $this->statuses = $statuses; 30 36 return $this; 31 37 } 32 38 ··· 119 125 $conn, 120 126 'charge.cartPHID IN (%Ls)', 121 127 $this->cartPHIDs); 128 + } 129 + 130 + if ($this->statuses !== null) { 131 + $where[] = qsprintf( 132 + $conn, 133 + 'charge.status IN (%Ls)', 134 + $this->statuses); 122 135 } 123 136 124 137 return $this->formatWhereClause($where);
+59 -9
src/applications/phortune/storage/PhortuneCart.php
··· 52 52 return $this; 53 53 } 54 54 55 - public function didApplyCharge(PhortuneCharge $charge) { 56 - if ($this->getStatus() !== self::STATUS_PURCHASING) { 57 - throw new Exception( 58 - pht( 59 - 'Cart has wrong status ("%s") to call didApplyCharge(), expected '. 60 - '"%s".', 61 - $this->getStatus(), 62 - self::STATUS_PURCHASING)); 55 + public function willApplyCharge( 56 + PhabricatorUser $actor, 57 + PhortunePaymentProvider $provider, 58 + PhortunePaymentMethod $method = null) { 59 + 60 + $account = $this->getAccount(); 61 + 62 + $charge = PhortuneCharge::initializeNewCharge() 63 + ->setAccountPHID($account->getPHID()) 64 + ->setCartPHID($this->getPHID()) 65 + ->setAuthorPHID($actor->getPHID()) 66 + ->setPaymentProviderKey($provider->getProviderKey()) 67 + ->setAmountAsCurrency($this->getTotalPriceAsCurrency()); 68 + 69 + if ($method) { 70 + $charge->setPaymentMethodPHID($method->getPHID()); 63 71 } 64 72 65 - $this->setStatus(self::STATUS_CHARGED)->save(); 73 + $this->openTransaction(); 74 + $this->beginReadLocking(); 75 + 76 + $copy = clone $this; 77 + $copy->reload(); 78 + 79 + if ($copy->getStatus() !== self::STATUS_READY) { 80 + throw new Exception( 81 + pht( 82 + 'Cart has wrong status ("%s") to call willApplyCharge(), expected '. 83 + '"%s".', 84 + $copy->getStatus(), 85 + self::STATUS_READY)); 86 + } 87 + 88 + $charge->save(); 89 + $this->setStatus(PhortuneCart::STATUS_PURCHASING)->save(); 90 + $this->saveTransaction(); 91 + 92 + return $charge; 93 + } 94 + 95 + public function didApplyCharge(PhortuneCharge $charge) { 96 + $charge->setStatus(PhortuneCharge::STATUS_CHARGED); 97 + 98 + $this->openTransaction(); 99 + $this->beginReadLocking(); 100 + 101 + $copy = clone $this; 102 + $copy->reload(); 103 + 104 + if ($copy->getStatus() !== self::STATUS_PURCHASING) { 105 + throw new Exception( 106 + pht( 107 + 'Cart has wrong status ("%s") to call didApplyCharge(), expected '. 108 + '"%s".', 109 + $copy->getStatus(), 110 + self::STATUS_PURCHASING)); 111 + } 112 + 113 + $charge->save(); 114 + $this->setStatus(self::STATUS_CHARGED)->save(); 115 + $this->saveTransaction(); 66 116 67 117 foreach ($this->purchases as $purchase) { 68 118 $purchase->getProduct()->didPurchaseProduct($purchase);
+5 -2
src/applications/phortune/storage/PhortuneCharge.php
··· 9 9 final class PhortuneCharge extends PhortuneDAO 10 10 implements PhabricatorPolicyInterface { 11 11 12 - const STATUS_PENDING = 'charge:pending'; 13 - const STATUS_AUTHORIZED = 'charge:authorized'; 14 12 const STATUS_CHARGING = 'charge:charging'; 15 13 const STATUS_CHARGED = 'charge:charged'; 16 14 const STATUS_FAILED = 'charge:failed'; ··· 26 24 27 25 private $account = self::ATTACHABLE; 28 26 private $cart = self::ATTACHABLE; 27 + 28 + public static function initializeNewCharge() { 29 + return id(new PhortuneCharge()) 30 + ->setStatus(self::STATUS_CHARGING); 31 + } 29 32 30 33 public function getConfiguration() { 31 34 return array(