1
0
Fork 0
mirror of https://we.phorge.it/source/phorge.git synced 2024-12-22 05:20:56 +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. * @return AphrontDialogView New dialog.
*/ */
protected function newDialog() { public function newDialog() {
$submit_uri = new PhutilURI($this->getRequest()->getRequestURI()); $submit_uri = new PhutilURI($this->getRequest()->getRequestURI());
$submit_uri = $submit_uri->getPath(); $submit_uri = $submit_uri->getPath();

View file

@ -111,7 +111,20 @@ final class PhortuneCartCheckoutController
$provider = $method->buildPaymentProvider(); $provider = $method->buildPaymentProvider();
$charge = $cart->willApplyCharge($viewer, $provider, $method); $charge = $cart->willApplyCharge($viewer, $provider, $method);
try {
$provider->applyCharge($method, $charge); $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); $cart->didApplyCharge($charge);
$done_uri = $cart->getDoneURI(); $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. // difficult for now and we can't reasonably just fail these charges.
var_dump($result); var_dump($result);
die(); 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': case 'cancel':
var_dump($_REQUEST); var_dump($_REQUEST);
break; break;

View file

@ -145,7 +145,7 @@ final class PhortuneWePayPaymentProvider extends PhortunePaymentProvider {
} }
public function getPaymentMethodDescription() { public function getPaymentMethodDescription() {
return pht('Credit Card or Bank Account'); return pht('Credit or Debit Card');
} }
public function getPaymentMethodIcon() { public function getPaymentMethodIcon() {
@ -286,10 +286,10 @@ final class PhortuneWePayPaymentProvider extends PhortunePaymentProvider {
$params = array( $params = array(
'account_id' => $this->getWePayAccountID(), 'account_id' => $this->getWePayAccountID(),
'short_description' => 'Services', // TODO 'short_description' => $cart->getName(),
'type' => 'SERVICE', 'type' => 'SERVICE',
'amount' => $price->formatBareValue(), 'amount' => $price->formatBareValue(),
'long_description' => 'Services', // TODO 'long_description' => $cart->getName(),
'reference_id' => $cart->getPHID(), 'reference_id' => $cart->getPHID(),
'app_fee' => 0, 'app_fee' => 0,
'fee_payer' => 'Payee', 'fee_payer' => 'Payee',
@ -305,7 +305,10 @@ final class PhortuneWePayPaymentProvider extends PhortunePaymentProvider {
'shipping_fee' => 0, 'shipping_fee' => 0,
'charge_tax' => 0, 'charge_tax' => 0,
'mode' => 'regular', '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); $charge = $cart->willApplyCharge($viewer, $this);
@ -322,6 +325,11 @@ final class PhortuneWePayPaymentProvider extends PhortunePaymentProvider {
->setIsExternal(true) ->setIsExternal(true)
->setURI($uri); ->setURI($uri);
case 'charge': case 'charge':
if ($cart->getStatus() !== PhortuneCart::STATUS_PURCHASING) {
return id(new AphrontRedirectResponse())
->setURI($cart->getCheckoutURI());
}
$checkout_id = $request->getInt('checkout_id'); $checkout_id = $request->getInt('checkout_id');
$params = array( $params = array(
'checkout_id' => $checkout_id, 'checkout_id' => $checkout_id,
@ -333,24 +341,41 @@ final class PhortuneWePayPaymentProvider extends PhortunePaymentProvider {
pht('Checkout reference ID does not match cart PHID!')); pht('Checkout reference ID does not match cart PHID!'));
} }
$unguarded = AphrontWriteGuard::beginScopedUnguardedWrites();
switch ($checkout->state) { switch ($checkout->state) {
case 'authorized': case 'authorized':
case 'reserved': case 'reserved':
case 'captured': 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; break;
default: default:
throw new Exception( // It's not clear if we can ever get here on the web workflow,
pht( // WePay doesn't seem to return back to us after a failure (the
'Checkout is in bad state "%s"!', // workflow dead-ends instead).
$result->state));
}
$unguarded = AphrontWriteGuard::beginScopedUnguardedWrites(); $cart->didFailCharge($charge);
$cart->didApplyCharge($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); unset($unguarded);
return id(new AphrontRedirectResponse()) return $response;
->setURI($cart->getDoneURI());
case 'cancel': case 'cancel':
// TODO: I don't know how it's possible to cancel out of a WePay // TODO: I don't know how it's possible to cancel out of a WePay
// charge workflow. // charge workflow.

View file

@ -146,6 +146,37 @@ final class PhortuneCart extends PhortuneDAO
return $this; 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( public function willRefundCharge(
PhabricatorUser $actor, PhabricatorUser $actor,
PhortunePaymentProvider $provider, PhortunePaymentProvider $provider,