mirror of
https://we.phorge.it/source/phorge.git
synced 2024-11-19 05:12:41 +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:
parent
fe5bc764b3
commit
38927d5704
10 changed files with 155 additions and 6 deletions
|
@ -2557,6 +2557,7 @@ phutil_register_library_map(array(
|
|||
'PhortuneAccountViewController' => 'applications/phortune/controller/PhortuneAccountViewController.php',
|
||||
'PhortuneBalancedPaymentProvider' => 'applications/phortune/provider/PhortuneBalancedPaymentProvider.php',
|
||||
'PhortuneCart' => 'applications/phortune/storage/PhortuneCart.php',
|
||||
'PhortuneCartAcceptController' => 'applications/phortune/controller/PhortuneCartAcceptController.php',
|
||||
'PhortuneCartCancelController' => 'applications/phortune/controller/PhortuneCartCancelController.php',
|
||||
'PhortuneCartCheckoutController' => 'applications/phortune/controller/PhortuneCartCheckoutController.php',
|
||||
'PhortuneCartController' => 'applications/phortune/controller/PhortuneCartController.php',
|
||||
|
@ -5616,6 +5617,7 @@ phutil_register_library_map(array(
|
|||
'PhortuneDAO',
|
||||
'PhabricatorPolicyInterface',
|
||||
),
|
||||
'PhortuneCartAcceptController' => 'PhortuneCartController',
|
||||
'PhortuneCartCancelController' => 'PhortuneCartController',
|
||||
'PhortuneCartCheckoutController' => 'PhortuneCartController',
|
||||
'PhortuneCartController' => 'PhortuneController',
|
||||
|
|
|
@ -50,6 +50,7 @@ final class PhabricatorPhortuneApplication extends PhabricatorApplication {
|
|||
'checkout/' => 'PhortuneCartCheckoutController',
|
||||
'(?P<action>cancel|refund)/' => 'PhortuneCartCancelController',
|
||||
'update/' => 'PhortuneCartUpdateController',
|
||||
'accept/' => 'PhortuneCartAcceptController',
|
||||
),
|
||||
'account/' => array(
|
||||
'' => 'PhortuneAccountListController',
|
||||
|
|
|
@ -176,6 +176,7 @@ final class PhortuneAccountViewController extends PhortuneController {
|
|||
PhortuneCart::STATUS_PURCHASING,
|
||||
PhortuneCart::STATUS_CHARGED,
|
||||
PhortuneCart::STATUS_HOLD,
|
||||
PhortuneCart::STATUS_REVIEW,
|
||||
PhortuneCart::STATUS_PURCHASED,
|
||||
))
|
||||
->execute();
|
||||
|
|
|
@ -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'));
|
||||
}
|
||||
}
|
|
@ -158,8 +158,9 @@ final class PhortuneCartCancelController
|
|||
}
|
||||
|
||||
// TODO: If every HOLD and CHARGING transaction has been fully refunded
|
||||
// and we're in a HOLD, PURCHASING or CHARGED cart state we probably
|
||||
// need to kick the cart back to READY here?
|
||||
// and we're in a HOLD, REVIEW, PURCHASING or CHARGED cart state we
|
||||
// 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);
|
||||
}
|
||||
|
@ -170,6 +171,7 @@ final class PhortuneCartCancelController
|
|||
$body = pht(
|
||||
'Really refund this order?');
|
||||
$button = pht('Refund Order');
|
||||
$cancel_text = pht('Cancel');
|
||||
|
||||
$form = id(new AphrontFormView())
|
||||
->setUser($viewer)
|
||||
|
@ -181,6 +183,7 @@ final class PhortuneCartCancelController
|
|||
->setValue($v_refund));
|
||||
|
||||
$form = $form->buildLayoutView();
|
||||
|
||||
} else {
|
||||
$title = pht('Cancel Order?');
|
||||
$body = pht(
|
||||
|
|
|
@ -42,6 +42,7 @@ final class PhortuneCartCheckoutController
|
|||
case PhortuneCart::STATUS_CHARGED:
|
||||
case PhortuneCart::STATUS_PURCHASING:
|
||||
case PhortuneCart::STATUS_HOLD:
|
||||
case PhortuneCart::STATUS_REVIEW:
|
||||
case PhortuneCart::STATUS_PURCHASED:
|
||||
// For these states, kick the user to the order page to give them
|
||||
// information and options.
|
||||
|
|
|
@ -72,6 +72,19 @@ final class PhortuneCartViewController
|
|||
phutil_tag('strong', array(), pht('Update Status')));
|
||||
}
|
||||
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:
|
||||
$error_view = id(new AphrontErrorView())
|
||||
->setSeverity(AphrontErrorView::SEVERITY_NOTICE)
|
||||
|
@ -197,6 +210,7 @@ final class PhortuneCartViewController
|
|||
$cancel_uri = $this->getApplicationURI("cart/{$id}/cancel/");
|
||||
$refund_uri = $this->getApplicationURI("cart/{$id}/refund/");
|
||||
$update_uri = $this->getApplicationURI("cart/{$id}/update/");
|
||||
$accept_uri = $this->getApplicationURI("cart/{$id}/accept/");
|
||||
|
||||
$view->addAction(
|
||||
id(new PhabricatorActionView())
|
||||
|
@ -207,6 +221,15 @@ final class PhortuneCartViewController
|
|||
->setHref($cancel_uri));
|
||||
|
||||
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(
|
||||
id(new PhabricatorActionView())
|
||||
->setName(pht('Refund Order'))
|
||||
|
|
|
@ -472,7 +472,7 @@ final class PhortunePayPalPaymentProvider extends PhortunePaymentProvider {
|
|||
|
||||
return $response;
|
||||
case 'cancel':
|
||||
if ($cart->getStatus() !== PhortuneCart::STATUS_PURCHASING) {
|
||||
if ($cart->getStatus() === PhortuneCart::STATUS_PURCHASING) {
|
||||
$unguarded = AphrontWriteGuard::beginScopedUnguardedWrites();
|
||||
// TODO: Since the user cancelled this, we could conceivably just
|
||||
// throw it away or make it more clear that it's a user cancel.
|
||||
|
|
|
@ -31,6 +31,8 @@ final class PhortuneCartSearchEngine
|
|||
array(
|
||||
PhortuneCart::STATUS_PURCHASING,
|
||||
PhortuneCart::STATUS_CHARGED,
|
||||
PhortuneCart::STATUS_HOLD,
|
||||
PhortuneCart::STATUS_REVIEW,
|
||||
PhortuneCart::STATUS_PURCHASED,
|
||||
));
|
||||
|
||||
|
|
|
@ -8,6 +8,7 @@ final class PhortuneCart extends PhortuneDAO
|
|||
const STATUS_PURCHASING = 'cart:purchasing';
|
||||
const STATUS_CHARGED = 'cart:charged';
|
||||
const STATUS_HOLD = 'cart:hold';
|
||||
const STATUS_REVIEW = 'cart:review';
|
||||
const STATUS_PURCHASED = 'cart:purchased';
|
||||
|
||||
protected $accountPHID;
|
||||
|
@ -59,6 +60,7 @@ final class PhortuneCart extends PhortuneDAO
|
|||
self::STATUS_PURCHASING => pht('Purchasing'),
|
||||
self::STATUS_CHARGED => pht('Charged'),
|
||||
self::STATUS_HOLD => pht('Hold'),
|
||||
self::STATUS_REVIEW => pht('Review'),
|
||||
self::STATUS_PURCHASED => pht('Purchased'),
|
||||
);
|
||||
}
|
||||
|
@ -163,11 +165,68 @@ final class PhortuneCart extends PhortuneDAO
|
|||
$this->endReadLocking();
|
||||
$this->saveTransaction();
|
||||
|
||||
foreach ($this->purchases as $purchase) {
|
||||
$purchase->getProduct()->didPurchaseProduct($purchase);
|
||||
// TODO: Perform purchase review. Here, we would apply rules to determine
|
||||
// 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;
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue