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:
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',
|
'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',
|
||||||
|
|
|
@ -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',
|
||||||
|
|
|
@ -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();
|
||||||
|
|
|
@ -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
|
// 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(
|
||||||
|
|
|
@ -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.
|
||||||
|
|
|
@ -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'))
|
||||||
|
|
|
@ -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.
|
||||||
|
|
|
@ -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,
|
||||||
));
|
));
|
||||||
|
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue