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:
parent
c0848bca6d
commit
ad991b0197
5 changed files with 110 additions and 23 deletions
|
@ -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();
|
||||||
|
|
||||||
|
|
|
@ -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();
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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.
|
||||||
|
|
|
@ -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,
|
||||||
|
|
Loading…
Reference in a new issue