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

Handle Phortune charge failures cleanly

Summary:
Ref T2787. Currently, we kill a cart and dead-end the workflow on a charge failure.

Instead, fail the charge and reset the cart so the user can try using a valid payment instrument like a normal checkout workflow would.

Some shakiness/smoothing on WePay for the moment; PayPal is still made up since we don't have a "Hold" state yet.

Test Plan: {F215214}

Reviewers: btrahan

Reviewed By: btrahan

Subscribers: epriestley

Maniphest Tasks: T2787

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

+110 -23
+1 -1
src/applications/base/controller/PhabricatorController.php
··· 518 518 * 519 519 * @return AphrontDialogView New dialog. 520 520 */ 521 - protected function newDialog() { 521 + public function newDialog() { 522 522 $submit_uri = new PhutilURI($this->getRequest()->getRequestURI()); 523 523 $submit_uri = $submit_uri->getPath(); 524 524
+14 -1
src/applications/phortune/controller/PhortuneCartCheckoutController.php
··· 111 111 $provider = $method->buildPaymentProvider(); 112 112 113 113 $charge = $cart->willApplyCharge($viewer, $provider, $method); 114 - $provider->applyCharge($method, $charge); 114 + 115 + try { 116 + $provider->applyCharge($method, $charge); 117 + } catch (Exception $ex) { 118 + $cart->didFailCharge($charge); 119 + return $this->newDialog() 120 + ->setTitle(pht('Charge Failed')) 121 + ->appendParagraph( 122 + pht( 123 + 'Unable to make payment: %s', 124 + $ex->getMessage())) 125 + ->addCancelButton($cart->getCheckoutURI(), pht('Continue')); 126 + } 127 + 115 128 $cart->didApplyCharge($charge); 116 129 117 130 $done_uri = $cart->getDoneURI();
+20 -2
src/applications/phortune/provider/PhortunePayPalPaymentProvider.php
··· 339 339 // difficult for now and we can't reasonably just fail these charges. 340 340 341 341 var_dump($result); 342 + die(); 342 343 343 - die(); 344 - break; 344 + $success = false; // TODO: <---- 345 + 346 + // TODO: Clean this up once that mess up there ^^^^^ gets cleaned up. 347 + $unguarded = AphrontWriteGuard::beginScopedUnguardedWrites(); 348 + if ($success) { 349 + $cart->didApplyCharge($charge); 350 + $response = id(new AphrontRedirectResponse())->setURI( 351 + $cart->getDoneURI()); 352 + } else { 353 + $cart->didFailCharge($charge); 354 + 355 + $response = $controller 356 + ->newDialog() 357 + ->setTitle(pht('Charge Failed')) 358 + ->addCancelButton($cart->getCheckoutURI(), pht('Continue')); 359 + } 360 + unset($unguarded); 361 + 362 + return $response; 345 363 case 'cancel': 346 364 var_dump($_REQUEST); 347 365 break;
+44 -19
src/applications/phortune/provider/PhortuneWePayPaymentProvider.php
··· 145 145 } 146 146 147 147 public function getPaymentMethodDescription() { 148 - return pht('Credit Card or Bank Account'); 148 + return pht('Credit or Debit Card'); 149 149 } 150 150 151 151 public function getPaymentMethodIcon() { ··· 286 286 287 287 $params = array( 288 288 'account_id' => $this->getWePayAccountID(), 289 - 'short_description' => 'Services', // TODO 289 + 'short_description' => $cart->getName(), 290 290 'type' => 'SERVICE', 291 291 'amount' => $price->formatBareValue(), 292 - 'long_description' => 'Services', // TODO 292 + 'long_description' => $cart->getName(), 293 293 'reference_id' => $cart->getPHID(), 294 294 'app_fee' => 0, 295 295 'fee_payer' => 'Payee', ··· 305 305 'shipping_fee' => 0, 306 306 'charge_tax' => 0, 307 307 'mode' => 'regular', 308 - 'funding_sources' => 'bank,cc', 308 + 309 + // TODO: We could accept bank accounts but the hold/capture rules 310 + // are not quite clear. Just accept credit cards for now. 311 + 'funding_sources' => 'cc', 309 312 ); 310 313 311 314 $charge = $cart->willApplyCharge($viewer, $this); ··· 322 325 ->setIsExternal(true) 323 326 ->setURI($uri); 324 327 case 'charge': 328 + if ($cart->getStatus() !== PhortuneCart::STATUS_PURCHASING) { 329 + return id(new AphrontRedirectResponse()) 330 + ->setURI($cart->getCheckoutURI()); 331 + } 332 + 325 333 $checkout_id = $request->getInt('checkout_id'); 326 334 $params = array( 327 335 'checkout_id' => $checkout_id, ··· 333 341 pht('Checkout reference ID does not match cart PHID!')); 334 342 } 335 343 336 - switch ($checkout->state) { 337 - case 'authorized': 338 - case 'reserved': 339 - case 'captured': 340 - break; 341 - default: 342 - throw new Exception( 343 - pht( 344 - 'Checkout is in bad state "%s"!', 345 - $result->state)); 346 - } 344 + $unguarded = AphrontWriteGuard::beginScopedUnguardedWrites(); 345 + switch ($checkout->state) { 346 + case 'authorized': 347 + case 'reserved': 348 + case 'captured': 349 + // TODO: Are these all really "done" states, and not "hold" 350 + // states? Cards and bank accounts both come back as "authorized" 351 + // on the staging environment. Figure out what happens in 352 + // production? 353 + 354 + $cart->didApplyCharge($charge); 355 + 356 + $response = id(new AphrontRedirectResponse())->setURI( 357 + $cart->getDoneURI()); 358 + break; 359 + default: 360 + // It's not clear if we can ever get here on the web workflow, 361 + // WePay doesn't seem to return back to us after a failure (the 362 + // workflow dead-ends instead). 347 363 348 - $unguarded = AphrontWriteGuard::beginScopedUnguardedWrites(); 349 - $cart->didApplyCharge($charge); 364 + $cart->didFailCharge($charge); 365 + 366 + $response = $controller 367 + ->newDialog() 368 + ->setTitle(pht('Charge Failed')) 369 + ->appendParagraph( 370 + pht( 371 + 'Unable to make payment (checkout state is "%s").', 372 + $checkout->state)) 373 + ->addCancelButton($cart->getCheckoutURI(), pht('Continue')); 374 + break; 375 + } 350 376 unset($unguarded); 351 377 352 - return id(new AphrontRedirectResponse()) 353 - ->setURI($cart->getDoneURI()); 378 + return $response; 354 379 case 'cancel': 355 380 // TODO: I don't know how it's possible to cancel out of a WePay 356 381 // charge workflow.
+31
src/applications/phortune/storage/PhortuneCart.php
··· 146 146 return $this; 147 147 } 148 148 149 + public function didFailCharge(PhortuneCharge $charge) { 150 + $charge->setStatus(PhortuneCharge::STATUS_FAILED); 151 + 152 + $this->openTransaction(); 153 + $this->beginReadLocking(); 154 + 155 + $copy = clone $this; 156 + $copy->reload(); 157 + 158 + if ($copy->getStatus() !== self::STATUS_PURCHASING) { 159 + throw new Exception( 160 + pht( 161 + 'Cart has wrong status ("%s") to call didFailCharge(), '. 162 + 'expected "%s".', 163 + $copy->getStatus(), 164 + self::STATUS_PURCHASING)); 165 + } 166 + 167 + $charge->save(); 168 + 169 + // Move the cart back into STATUS_READY so the user can try 170 + // making the purchase again. 171 + $this->setStatus(self::STATUS_READY)->save(); 172 + 173 + $this->endReadLocking(); 174 + $this->saveTransaction(); 175 + 176 + return $this; 177 + } 178 + 179 + 149 180 public function willRefundCharge( 150 181 PhabricatorUser $actor, 151 182 PhortunePaymentProvider $provider,