1
0
Fork 0
mirror of https://we.phorge.it/source/phorge.git synced 2024-11-10 08:52:39 +01:00

Add a "Review" status to Phortune

Summary: Ref T2787. Allow merchants to flag orders for review. For now, all orders are flagged for review. Eventually, I could imagine Herald rules for coarse things (e.g., require review of all orders over $1,000, or require review of all orders by users not on a whitelist) and maybe examining fraud data for the providers which support it.

Test Plan: {F215848}

Reviewers: btrahan

Reviewed By: btrahan

Subscribers: epriestley

Maniphest Tasks: T2787

Differential Revision: https://secure.phabricator.com/D10675
This commit is contained in:
epriestley 2014-10-10 11:29:13 -07:00
parent fe5bc764b3
commit 38927d5704
10 changed files with 155 additions and 6 deletions

View file

@ -2557,6 +2557,7 @@ phutil_register_library_map(array(
'PhortuneAccountViewController' => 'applications/phortune/controller/PhortuneAccountViewController.php', 'PhortuneAccountViewController' => 'applications/phortune/controller/PhortuneAccountViewController.php',
'PhortuneBalancedPaymentProvider' => 'applications/phortune/provider/PhortuneBalancedPaymentProvider.php', 'PhortuneBalancedPaymentProvider' => 'applications/phortune/provider/PhortuneBalancedPaymentProvider.php',
'PhortuneCart' => 'applications/phortune/storage/PhortuneCart.php', 'PhortuneCart' => 'applications/phortune/storage/PhortuneCart.php',
'PhortuneCartAcceptController' => 'applications/phortune/controller/PhortuneCartAcceptController.php',
'PhortuneCartCancelController' => 'applications/phortune/controller/PhortuneCartCancelController.php', 'PhortuneCartCancelController' => 'applications/phortune/controller/PhortuneCartCancelController.php',
'PhortuneCartCheckoutController' => 'applications/phortune/controller/PhortuneCartCheckoutController.php', 'PhortuneCartCheckoutController' => 'applications/phortune/controller/PhortuneCartCheckoutController.php',
'PhortuneCartController' => 'applications/phortune/controller/PhortuneCartController.php', 'PhortuneCartController' => 'applications/phortune/controller/PhortuneCartController.php',
@ -5616,6 +5617,7 @@ phutil_register_library_map(array(
'PhortuneDAO', 'PhortuneDAO',
'PhabricatorPolicyInterface', 'PhabricatorPolicyInterface',
), ),
'PhortuneCartAcceptController' => 'PhortuneCartController',
'PhortuneCartCancelController' => 'PhortuneCartController', 'PhortuneCartCancelController' => 'PhortuneCartController',
'PhortuneCartCheckoutController' => 'PhortuneCartController', 'PhortuneCartCheckoutController' => 'PhortuneCartController',
'PhortuneCartController' => 'PhortuneController', 'PhortuneCartController' => 'PhortuneController',

View file

@ -50,6 +50,7 @@ final class PhabricatorPhortuneApplication extends PhabricatorApplication {
'checkout/' => 'PhortuneCartCheckoutController', 'checkout/' => 'PhortuneCartCheckoutController',
'(?P<action>cancel|refund)/' => 'PhortuneCartCancelController', '(?P<action>cancel|refund)/' => 'PhortuneCartCancelController',
'update/' => 'PhortuneCartUpdateController', 'update/' => 'PhortuneCartUpdateController',
'accept/' => 'PhortuneCartAcceptController',
), ),
'account/' => array( 'account/' => array(
'' => 'PhortuneAccountListController', '' => 'PhortuneAccountListController',

View file

@ -176,6 +176,7 @@ final class PhortuneAccountViewController extends PhortuneController {
PhortuneCart::STATUS_PURCHASING, PhortuneCart::STATUS_PURCHASING,
PhortuneCart::STATUS_CHARGED, PhortuneCart::STATUS_CHARGED,
PhortuneCart::STATUS_HOLD, PhortuneCart::STATUS_HOLD,
PhortuneCart::STATUS_REVIEW,
PhortuneCart::STATUS_PURCHASED, PhortuneCart::STATUS_PURCHASED,
)) ))
->execute(); ->execute();

View file

@ -0,0 +1,57 @@
<?php
final class PhortuneCartAcceptController
extends PhortuneCartController {
private $id;
public function willProcessRequest(array $data) {
$this->id = $data['id'];
}
public function processRequest() {
$request = $this->getRequest();
$viewer = $request->getUser();
$cart = id(new PhortuneCartQuery())
->setViewer($viewer)
->withIDs(array($this->id))
->needPurchases(true)
->executeOne();
if (!$cart) {
return new Aphront404Response();
}
// You must control the merchant to accept orders.
PhabricatorPolicyFilter::requireCapability(
$viewer,
$cart->getMerchant(),
PhabricatorPolicyCapability::CAN_EDIT);
$cancel_uri = $cart->getDetailURI();
if ($cart->getStatus() !== PhortuneCart::STATUS_REVIEW) {
return $this->newDialog()
->setTitle(pht('Order Not in Review'))
->appendParagraph(
pht(
'This order does not need manual review, so you can not '.
'accept it.'))
->addCancelButton($cancel_uri);
}
if ($request->isFormPost()) {
$cart->didReviewCart();
return id(new AphrontRedirectResponse())->setURI($cancel_uri);
}
return $this->newDialog()
->setTitle(pht('Accept Order?'))
->appendParagraph(
pht(
'This order has been flagged for manual review. You should review '.
'it carefully before accepting it.'))
->addCancelButton($cancel_uri)
->addSubmitButton(pht('Accept Order'));
}
}

View file

@ -158,8 +158,9 @@ final class PhortuneCartCancelController
} }
// TODO: If every HOLD and CHARGING transaction has been fully refunded // TODO: If every HOLD and CHARGING transaction has been fully refunded
// and we're in a HOLD, PURCHASING or CHARGED cart state we probably // and we're in a HOLD, REVIEW, PURCHASING or CHARGED cart state we
// need to kick the cart back to READY here? // probably need to kick the cart back to READY here (or maybe kill
// it if it was in REVIEW)?
return id(new AphrontRedirectResponse())->setURI($cancel_uri); return id(new AphrontRedirectResponse())->setURI($cancel_uri);
} }
@ -170,6 +171,7 @@ final class PhortuneCartCancelController
$body = pht( $body = pht(
'Really refund this order?'); 'Really refund this order?');
$button = pht('Refund Order'); $button = pht('Refund Order');
$cancel_text = pht('Cancel');
$form = id(new AphrontFormView()) $form = id(new AphrontFormView())
->setUser($viewer) ->setUser($viewer)
@ -181,6 +183,7 @@ final class PhortuneCartCancelController
->setValue($v_refund)); ->setValue($v_refund));
$form = $form->buildLayoutView(); $form = $form->buildLayoutView();
} else { } else {
$title = pht('Cancel Order?'); $title = pht('Cancel Order?');
$body = pht( $body = pht(

View file

@ -42,6 +42,7 @@ final class PhortuneCartCheckoutController
case PhortuneCart::STATUS_CHARGED: case PhortuneCart::STATUS_CHARGED:
case PhortuneCart::STATUS_PURCHASING: case PhortuneCart::STATUS_PURCHASING:
case PhortuneCart::STATUS_HOLD: case PhortuneCart::STATUS_HOLD:
case PhortuneCart::STATUS_REVIEW:
case PhortuneCart::STATUS_PURCHASED: case PhortuneCart::STATUS_PURCHASED:
// For these states, kick the user to the order page to give them // For these states, kick the user to the order page to give them
// information and options. // information and options.

View file

@ -72,6 +72,19 @@ final class PhortuneCartViewController
phutil_tag('strong', array(), pht('Update Status'))); phutil_tag('strong', array(), pht('Update Status')));
} }
break; break;
case PhortuneCart::STATUS_REVIEW:
if ($can_admin) {
$errors[] = pht(
'This order has been flagged for manual review. Review the order '.
'and choose %s to accept it or %s to reject it.',
phutil_tag('strong', array(), pht('Accept Order')),
phutil_tag('strong', array(), pht('Refund Order')));
} else if ($can_edit) {
$errors[] = pht(
'This order requires manual processing and will complete once '.
'the merchant accepts it.');
}
break;
case PhortuneCart::STATUS_PURCHASED: case PhortuneCart::STATUS_PURCHASED:
$error_view = id(new AphrontErrorView()) $error_view = id(new AphrontErrorView())
->setSeverity(AphrontErrorView::SEVERITY_NOTICE) ->setSeverity(AphrontErrorView::SEVERITY_NOTICE)
@ -197,6 +210,7 @@ final class PhortuneCartViewController
$cancel_uri = $this->getApplicationURI("cart/{$id}/cancel/"); $cancel_uri = $this->getApplicationURI("cart/{$id}/cancel/");
$refund_uri = $this->getApplicationURI("cart/{$id}/refund/"); $refund_uri = $this->getApplicationURI("cart/{$id}/refund/");
$update_uri = $this->getApplicationURI("cart/{$id}/update/"); $update_uri = $this->getApplicationURI("cart/{$id}/update/");
$accept_uri = $this->getApplicationURI("cart/{$id}/accept/");
$view->addAction( $view->addAction(
id(new PhabricatorActionView()) id(new PhabricatorActionView())
@ -207,6 +221,15 @@ final class PhortuneCartViewController
->setHref($cancel_uri)); ->setHref($cancel_uri));
if ($can_admin) { if ($can_admin) {
if ($cart->getStatus() == PhortuneCart::STATUS_REVIEW) {
$view->addAction(
id(new PhabricatorActionView())
->setName(pht('Accept Order'))
->setIcon('fa-check')
->setWorkflow(true)
->setHref($accept_uri));
}
$view->addAction( $view->addAction(
id(new PhabricatorActionView()) id(new PhabricatorActionView())
->setName(pht('Refund Order')) ->setName(pht('Refund Order'))

View file

@ -472,7 +472,7 @@ final class PhortunePayPalPaymentProvider extends PhortunePaymentProvider {
return $response; return $response;
case 'cancel': case 'cancel':
if ($cart->getStatus() !== PhortuneCart::STATUS_PURCHASING) { if ($cart->getStatus() === PhortuneCart::STATUS_PURCHASING) {
$unguarded = AphrontWriteGuard::beginScopedUnguardedWrites(); $unguarded = AphrontWriteGuard::beginScopedUnguardedWrites();
// TODO: Since the user cancelled this, we could conceivably just // TODO: Since the user cancelled this, we could conceivably just
// throw it away or make it more clear that it's a user cancel. // throw it away or make it more clear that it's a user cancel.

View file

@ -31,6 +31,8 @@ final class PhortuneCartSearchEngine
array( array(
PhortuneCart::STATUS_PURCHASING, PhortuneCart::STATUS_PURCHASING,
PhortuneCart::STATUS_CHARGED, PhortuneCart::STATUS_CHARGED,
PhortuneCart::STATUS_HOLD,
PhortuneCart::STATUS_REVIEW,
PhortuneCart::STATUS_PURCHASED, PhortuneCart::STATUS_PURCHASED,
)); ));

View file

@ -8,6 +8,7 @@ final class PhortuneCart extends PhortuneDAO
const STATUS_PURCHASING = 'cart:purchasing'; const STATUS_PURCHASING = 'cart:purchasing';
const STATUS_CHARGED = 'cart:charged'; const STATUS_CHARGED = 'cart:charged';
const STATUS_HOLD = 'cart:hold'; const STATUS_HOLD = 'cart:hold';
const STATUS_REVIEW = 'cart:review';
const STATUS_PURCHASED = 'cart:purchased'; const STATUS_PURCHASED = 'cart:purchased';
protected $accountPHID; protected $accountPHID;
@ -59,6 +60,7 @@ final class PhortuneCart extends PhortuneDAO
self::STATUS_PURCHASING => pht('Purchasing'), self::STATUS_PURCHASING => pht('Purchasing'),
self::STATUS_CHARGED => pht('Charged'), self::STATUS_CHARGED => pht('Charged'),
self::STATUS_HOLD => pht('Hold'), self::STATUS_HOLD => pht('Hold'),
self::STATUS_REVIEW => pht('Review'),
self::STATUS_PURCHASED => pht('Purchased'), self::STATUS_PURCHASED => pht('Purchased'),
); );
} }
@ -163,11 +165,68 @@ final class PhortuneCart extends PhortuneDAO
$this->endReadLocking(); $this->endReadLocking();
$this->saveTransaction(); $this->saveTransaction();
foreach ($this->purchases as $purchase) { // TODO: Perform purchase review. Here, we would apply rules to determine
$purchase->getProduct()->didPurchaseProduct($purchase); // whether the charge needs manual review (maybe making the decision via
// Herald, configuration, or by examining provider fraud data). For now,
// always require review.
$needs_review = true;
if ($needs_review) {
$this->willReviewCart();
} else {
$this->didReviewCart();
} }
$this->setStatus(self::STATUS_PURCHASED)->save(); return $this;
}
public function willReviewCart() {
$this->openTransaction();
$this->beginReadLocking();
$copy = clone $this;
$copy->reload();
if (($copy->getStatus() !== self::STATUS_CHARGED)) {
throw new Exception(
pht(
'Cart has wrong status ("%s") to call willReviewCart()!',
$copy->getStatus()));
}
$this->setStatus(self::STATUS_REVIEW)->save();
$this->endReadLocking();
$this->saveTransaction();
// TODO: Notify merchant to review order.
return $this;
}
public function didReviewCart() {
$this->openTransaction();
$this->beginReadLocking();
$copy = clone $this;
$copy->reload();
if (($copy->getStatus() !== self::STATUS_CHARGED) &&
($copy->getStatus() !== self::STATUS_REVIEW)) {
throw new Exception(
pht(
'Cart has wrong status ("%s") to call didReviewCart()!',
$copy->getStatus()));
}
foreach ($this->purchases as $purchase) {
$purchase->getProduct()->didPurchaseProduct($purchase);
}
$this->setStatus(self::STATUS_PURCHASED)->save();
$this->endReadLocking();
$this->saveTransaction();
return $this; return $this;
} }