1
0
Fork 0
mirror of https://we.phorge.it/source/phorge.git synced 2024-12-19 12:00:55 +01:00

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
This commit is contained in:
epriestley 2014-10-08 17:23:02 -07:00
parent c0848bca6d
commit ad991b0197
5 changed files with 110 additions and 23 deletions

View file

@ -518,7 +518,7 @@ abstract class PhabricatorController extends AphrontController {
*
* @return AphrontDialogView New dialog.
*/
protected function newDialog() {
public function newDialog() {
$submit_uri = new PhutilURI($this->getRequest()->getRequestURI());
$submit_uri = $submit_uri->getPath();

View file

@ -111,7 +111,20 @@ final class PhortuneCartCheckoutController
$provider = $method->buildPaymentProvider();
$charge = $cart->willApplyCharge($viewer, $provider, $method);
$provider->applyCharge($method, $charge);
try {
$provider->applyCharge($method, $charge);
} catch (Exception $ex) {
$cart->didFailCharge($charge);
return $this->newDialog()
->setTitle(pht('Charge Failed'))
->appendParagraph(
pht(
'Unable to make payment: %s',
$ex->getMessage()))
->addCancelButton($cart->getCheckoutURI(), pht('Continue'));
}
$cart->didApplyCharge($charge);
$done_uri = $cart->getDoneURI();

View file

@ -339,9 +339,27 @@ final class PhortunePayPalPaymentProvider extends PhortunePaymentProvider {
// difficult for now and we can't reasonably just fail these charges.
var_dump($result);
die();
break;
$success = false; // TODO: <----
// TODO: Clean this up once that mess up there ^^^^^ gets cleaned up.
$unguarded = AphrontWriteGuard::beginScopedUnguardedWrites();
if ($success) {
$cart->didApplyCharge($charge);
$response = id(new AphrontRedirectResponse())->setURI(
$cart->getDoneURI());
} else {
$cart->didFailCharge($charge);
$response = $controller
->newDialog()
->setTitle(pht('Charge Failed'))
->addCancelButton($cart->getCheckoutURI(), pht('Continue'));
}
unset($unguarded);
return $response;
case 'cancel':
var_dump($_REQUEST);
break;

View file

@ -145,7 +145,7 @@ final class PhortuneWePayPaymentProvider extends PhortunePaymentProvider {
}
public function getPaymentMethodDescription() {
return pht('Credit Card or Bank Account');
return pht('Credit or Debit Card');
}
public function getPaymentMethodIcon() {
@ -286,10 +286,10 @@ final class PhortuneWePayPaymentProvider extends PhortunePaymentProvider {
$params = array(
'account_id' => $this->getWePayAccountID(),
'short_description' => 'Services', // TODO
'short_description' => $cart->getName(),
'type' => 'SERVICE',
'amount' => $price->formatBareValue(),
'long_description' => 'Services', // TODO
'long_description' => $cart->getName(),
'reference_id' => $cart->getPHID(),
'app_fee' => 0,
'fee_payer' => 'Payee',
@ -305,7 +305,10 @@ final class PhortuneWePayPaymentProvider extends PhortunePaymentProvider {
'shipping_fee' => 0,
'charge_tax' => 0,
'mode' => 'regular',
'funding_sources' => 'bank,cc',
// TODO: We could accept bank accounts but the hold/capture rules
// are not quite clear. Just accept credit cards for now.
'funding_sources' => 'cc',
);
$charge = $cart->willApplyCharge($viewer, $this);
@ -322,6 +325,11 @@ final class PhortuneWePayPaymentProvider extends PhortunePaymentProvider {
->setIsExternal(true)
->setURI($uri);
case 'charge':
if ($cart->getStatus() !== PhortuneCart::STATUS_PURCHASING) {
return id(new AphrontRedirectResponse())
->setURI($cart->getCheckoutURI());
}
$checkout_id = $request->getInt('checkout_id');
$params = array(
'checkout_id' => $checkout_id,
@ -333,24 +341,41 @@ final class PhortuneWePayPaymentProvider extends PhortunePaymentProvider {
pht('Checkout reference ID does not match cart PHID!'));
}
switch ($checkout->state) {
case 'authorized':
case 'reserved':
case 'captured':
break;
default:
throw new Exception(
pht(
'Checkout is in bad state "%s"!',
$result->state));
}
$unguarded = AphrontWriteGuard::beginScopedUnguardedWrites();
$cart->didApplyCharge($charge);
switch ($checkout->state) {
case 'authorized':
case 'reserved':
case 'captured':
// TODO: Are these all really "done" states, and not "hold"
// states? Cards and bank accounts both come back as "authorized"
// on the staging environment. Figure out what happens in
// production?
$cart->didApplyCharge($charge);
$response = id(new AphrontRedirectResponse())->setURI(
$cart->getDoneURI());
break;
default:
// It's not clear if we can ever get here on the web workflow,
// WePay doesn't seem to return back to us after a failure (the
// workflow dead-ends instead).
$cart->didFailCharge($charge);
$response = $controller
->newDialog()
->setTitle(pht('Charge Failed'))
->appendParagraph(
pht(
'Unable to make payment (checkout state is "%s").',
$checkout->state))
->addCancelButton($cart->getCheckoutURI(), pht('Continue'));
break;
}
unset($unguarded);
return id(new AphrontRedirectResponse())
->setURI($cart->getDoneURI());
return $response;
case 'cancel':
// TODO: I don't know how it's possible to cancel out of a WePay
// charge workflow.

View file

@ -146,6 +146,37 @@ final class PhortuneCart extends PhortuneDAO
return $this;
}
public function didFailCharge(PhortuneCharge $charge) {
$charge->setStatus(PhortuneCharge::STATUS_FAILED);
$this->openTransaction();
$this->beginReadLocking();
$copy = clone $this;
$copy->reload();
if ($copy->getStatus() !== self::STATUS_PURCHASING) {
throw new Exception(
pht(
'Cart has wrong status ("%s") to call didFailCharge(), '.
'expected "%s".',
$copy->getStatus(),
self::STATUS_PURCHASING));
}
$charge->save();
// Move the cart back into STATUS_READY so the user can try
// making the purchase again.
$this->setStatus(self::STATUS_READY)->save();
$this->endReadLocking();
$this->saveTransaction();
return $this;
}
public function willRefundCharge(
PhabricatorUser $actor,
PhortunePaymentProvider $provider,