diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php --- a/src/__phutil_library_map__.php +++ b/src/__phutil_library_map__.php @@ -2557,6 +2557,7 @@ '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 @@ 'PhortuneDAO', 'PhabricatorPolicyInterface', ), + 'PhortuneCartAcceptController' => 'PhortuneCartController', 'PhortuneCartCancelController' => 'PhortuneCartController', 'PhortuneCartCheckoutController' => 'PhortuneCartController', 'PhortuneCartController' => 'PhortuneController', diff --git a/src/applications/phortune/application/PhabricatorPhortuneApplication.php b/src/applications/phortune/application/PhabricatorPhortuneApplication.php --- a/src/applications/phortune/application/PhabricatorPhortuneApplication.php +++ b/src/applications/phortune/application/PhabricatorPhortuneApplication.php @@ -50,6 +50,7 @@ 'checkout/' => 'PhortuneCartCheckoutController', '(?Pcancel|refund)/' => 'PhortuneCartCancelController', 'update/' => 'PhortuneCartUpdateController', + 'accept/' => 'PhortuneCartAcceptController', ), 'account/' => array( '' => 'PhortuneAccountListController', diff --git a/src/applications/phortune/controller/PhortuneAccountViewController.php b/src/applications/phortune/controller/PhortuneAccountViewController.php --- a/src/applications/phortune/controller/PhortuneAccountViewController.php +++ b/src/applications/phortune/controller/PhortuneAccountViewController.php @@ -176,6 +176,7 @@ PhortuneCart::STATUS_PURCHASING, PhortuneCart::STATUS_CHARGED, PhortuneCart::STATUS_HOLD, + PhortuneCart::STATUS_REVIEW, PhortuneCart::STATUS_PURCHASED, )) ->execute(); diff --git a/src/applications/phortune/controller/PhortuneCartAcceptController.php b/src/applications/phortune/controller/PhortuneCartAcceptController.php new file mode 100644 --- /dev/null +++ b/src/applications/phortune/controller/PhortuneCartAcceptController.php @@ -0,0 +1,57 @@ +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')); + } +} diff --git a/src/applications/phortune/controller/PhortuneCartCancelController.php b/src/applications/phortune/controller/PhortuneCartCancelController.php --- a/src/applications/phortune/controller/PhortuneCartCancelController.php +++ b/src/applications/phortune/controller/PhortuneCartCancelController.php @@ -158,8 +158,9 @@ } // 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 @@ $body = pht( 'Really refund this order?'); $button = pht('Refund Order'); + $cancel_text = pht('Cancel'); $form = id(new AphrontFormView()) ->setUser($viewer) @@ -181,6 +183,7 @@ ->setValue($v_refund)); $form = $form->buildLayoutView(); + } else { $title = pht('Cancel Order?'); $body = pht( diff --git a/src/applications/phortune/controller/PhortuneCartCheckoutController.php b/src/applications/phortune/controller/PhortuneCartCheckoutController.php --- a/src/applications/phortune/controller/PhortuneCartCheckoutController.php +++ b/src/applications/phortune/controller/PhortuneCartCheckoutController.php @@ -42,6 +42,7 @@ 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. diff --git a/src/applications/phortune/controller/PhortuneCartViewController.php b/src/applications/phortune/controller/PhortuneCartViewController.php --- a/src/applications/phortune/controller/PhortuneCartViewController.php +++ b/src/applications/phortune/controller/PhortuneCartViewController.php @@ -72,6 +72,19 @@ 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 @@ $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 @@ ->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')) diff --git a/src/applications/phortune/provider/PhortunePayPalPaymentProvider.php b/src/applications/phortune/provider/PhortunePayPalPaymentProvider.php --- a/src/applications/phortune/provider/PhortunePayPalPaymentProvider.php +++ b/src/applications/phortune/provider/PhortunePayPalPaymentProvider.php @@ -472,7 +472,7 @@ 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. diff --git a/src/applications/phortune/query/PhortuneCartSearchEngine.php b/src/applications/phortune/query/PhortuneCartSearchEngine.php --- a/src/applications/phortune/query/PhortuneCartSearchEngine.php +++ b/src/applications/phortune/query/PhortuneCartSearchEngine.php @@ -31,6 +31,8 @@ array( PhortuneCart::STATUS_PURCHASING, PhortuneCart::STATUS_CHARGED, + PhortuneCart::STATUS_HOLD, + PhortuneCart::STATUS_REVIEW, PhortuneCart::STATUS_PURCHASED, )); diff --git a/src/applications/phortune/storage/PhortuneCart.php b/src/applications/phortune/storage/PhortuneCart.php --- a/src/applications/phortune/storage/PhortuneCart.php +++ b/src/applications/phortune/storage/PhortuneCart.php @@ -8,6 +8,7 @@ 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 @@ 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 @@ $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; }