@recaptime-dev's working patches + fork for Phorge, a community fork of Phabricator. (Upstream dev and stable branches are at upstream/main and upstream/stable respectively.) hq.recaptime.dev/wiki/Phorge
phorge phabricator
1
fork

Configure Feed

Select the types of activity you want to include in your feed.

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

+155 -6
+2
src/__phutil_library_map__.php
··· 2557 2557 'PhortuneAccountViewController' => 'applications/phortune/controller/PhortuneAccountViewController.php', 2558 2558 'PhortuneBalancedPaymentProvider' => 'applications/phortune/provider/PhortuneBalancedPaymentProvider.php', 2559 2559 'PhortuneCart' => 'applications/phortune/storage/PhortuneCart.php', 2560 + 'PhortuneCartAcceptController' => 'applications/phortune/controller/PhortuneCartAcceptController.php', 2560 2561 'PhortuneCartCancelController' => 'applications/phortune/controller/PhortuneCartCancelController.php', 2561 2562 'PhortuneCartCheckoutController' => 'applications/phortune/controller/PhortuneCartCheckoutController.php', 2562 2563 'PhortuneCartController' => 'applications/phortune/controller/PhortuneCartController.php', ··· 5616 5617 'PhortuneDAO', 5617 5618 'PhabricatorPolicyInterface', 5618 5619 ), 5620 + 'PhortuneCartAcceptController' => 'PhortuneCartController', 5619 5621 'PhortuneCartCancelController' => 'PhortuneCartController', 5620 5622 'PhortuneCartCheckoutController' => 'PhortuneCartController', 5621 5623 'PhortuneCartController' => 'PhortuneController',
+1
src/applications/phortune/application/PhabricatorPhortuneApplication.php
··· 50 50 'checkout/' => 'PhortuneCartCheckoutController', 51 51 '(?P<action>cancel|refund)/' => 'PhortuneCartCancelController', 52 52 'update/' => 'PhortuneCartUpdateController', 53 + 'accept/' => 'PhortuneCartAcceptController', 53 54 ), 54 55 'account/' => array( 55 56 '' => 'PhortuneAccountListController',
+1
src/applications/phortune/controller/PhortuneAccountViewController.php
··· 176 176 PhortuneCart::STATUS_PURCHASING, 177 177 PhortuneCart::STATUS_CHARGED, 178 178 PhortuneCart::STATUS_HOLD, 179 + PhortuneCart::STATUS_REVIEW, 179 180 PhortuneCart::STATUS_PURCHASED, 180 181 )) 181 182 ->execute();
+57
src/applications/phortune/controller/PhortuneCartAcceptController.php
··· 1 + <?php 2 + 3 + final class PhortuneCartAcceptController 4 + extends PhortuneCartController { 5 + 6 + private $id; 7 + 8 + public function willProcessRequest(array $data) { 9 + $this->id = $data['id']; 10 + } 11 + 12 + public function processRequest() { 13 + $request = $this->getRequest(); 14 + $viewer = $request->getUser(); 15 + 16 + $cart = id(new PhortuneCartQuery()) 17 + ->setViewer($viewer) 18 + ->withIDs(array($this->id)) 19 + ->needPurchases(true) 20 + ->executeOne(); 21 + if (!$cart) { 22 + return new Aphront404Response(); 23 + } 24 + 25 + // You must control the merchant to accept orders. 26 + PhabricatorPolicyFilter::requireCapability( 27 + $viewer, 28 + $cart->getMerchant(), 29 + PhabricatorPolicyCapability::CAN_EDIT); 30 + 31 + $cancel_uri = $cart->getDetailURI(); 32 + 33 + if ($cart->getStatus() !== PhortuneCart::STATUS_REVIEW) { 34 + return $this->newDialog() 35 + ->setTitle(pht('Order Not in Review')) 36 + ->appendParagraph( 37 + pht( 38 + 'This order does not need manual review, so you can not '. 39 + 'accept it.')) 40 + ->addCancelButton($cancel_uri); 41 + } 42 + 43 + if ($request->isFormPost()) { 44 + $cart->didReviewCart(); 45 + return id(new AphrontRedirectResponse())->setURI($cancel_uri); 46 + } 47 + 48 + return $this->newDialog() 49 + ->setTitle(pht('Accept Order?')) 50 + ->appendParagraph( 51 + pht( 52 + 'This order has been flagged for manual review. You should review '. 53 + 'it carefully before accepting it.')) 54 + ->addCancelButton($cancel_uri) 55 + ->addSubmitButton(pht('Accept Order')); 56 + } 57 + }
+5 -2
src/applications/phortune/controller/PhortuneCartCancelController.php
··· 158 158 } 159 159 160 160 // TODO: If every HOLD and CHARGING transaction has been fully refunded 161 - // and we're in a HOLD, PURCHASING or CHARGED cart state we probably 162 - // need to kick the cart back to READY here? 161 + // and we're in a HOLD, REVIEW, PURCHASING or CHARGED cart state we 162 + // probably need to kick the cart back to READY here (or maybe kill 163 + // it if it was in REVIEW)? 163 164 164 165 return id(new AphrontRedirectResponse())->setURI($cancel_uri); 165 166 } ··· 170 171 $body = pht( 171 172 'Really refund this order?'); 172 173 $button = pht('Refund Order'); 174 + $cancel_text = pht('Cancel'); 173 175 174 176 $form = id(new AphrontFormView()) 175 177 ->setUser($viewer) ··· 181 183 ->setValue($v_refund)); 182 184 183 185 $form = $form->buildLayoutView(); 186 + 184 187 } else { 185 188 $title = pht('Cancel Order?'); 186 189 $body = pht(
+1
src/applications/phortune/controller/PhortuneCartCheckoutController.php
··· 42 42 case PhortuneCart::STATUS_CHARGED: 43 43 case PhortuneCart::STATUS_PURCHASING: 44 44 case PhortuneCart::STATUS_HOLD: 45 + case PhortuneCart::STATUS_REVIEW: 45 46 case PhortuneCart::STATUS_PURCHASED: 46 47 // For these states, kick the user to the order page to give them 47 48 // information and options.
+23
src/applications/phortune/controller/PhortuneCartViewController.php
··· 72 72 phutil_tag('strong', array(), pht('Update Status'))); 73 73 } 74 74 break; 75 + case PhortuneCart::STATUS_REVIEW: 76 + if ($can_admin) { 77 + $errors[] = pht( 78 + 'This order has been flagged for manual review. Review the order '. 79 + 'and choose %s to accept it or %s to reject it.', 80 + phutil_tag('strong', array(), pht('Accept Order')), 81 + phutil_tag('strong', array(), pht('Refund Order'))); 82 + } else if ($can_edit) { 83 + $errors[] = pht( 84 + 'This order requires manual processing and will complete once '. 85 + 'the merchant accepts it.'); 86 + } 87 + break; 75 88 case PhortuneCart::STATUS_PURCHASED: 76 89 $error_view = id(new AphrontErrorView()) 77 90 ->setSeverity(AphrontErrorView::SEVERITY_NOTICE) ··· 197 210 $cancel_uri = $this->getApplicationURI("cart/{$id}/cancel/"); 198 211 $refund_uri = $this->getApplicationURI("cart/{$id}/refund/"); 199 212 $update_uri = $this->getApplicationURI("cart/{$id}/update/"); 213 + $accept_uri = $this->getApplicationURI("cart/{$id}/accept/"); 200 214 201 215 $view->addAction( 202 216 id(new PhabricatorActionView()) ··· 207 221 ->setHref($cancel_uri)); 208 222 209 223 if ($can_admin) { 224 + if ($cart->getStatus() == PhortuneCart::STATUS_REVIEW) { 225 + $view->addAction( 226 + id(new PhabricatorActionView()) 227 + ->setName(pht('Accept Order')) 228 + ->setIcon('fa-check') 229 + ->setWorkflow(true) 230 + ->setHref($accept_uri)); 231 + } 232 + 210 233 $view->addAction( 211 234 id(new PhabricatorActionView()) 212 235 ->setName(pht('Refund Order'))
+1 -1
src/applications/phortune/provider/PhortunePayPalPaymentProvider.php
··· 472 472 473 473 return $response; 474 474 case 'cancel': 475 - if ($cart->getStatus() !== PhortuneCart::STATUS_PURCHASING) { 475 + if ($cart->getStatus() === PhortuneCart::STATUS_PURCHASING) { 476 476 $unguarded = AphrontWriteGuard::beginScopedUnguardedWrites(); 477 477 // TODO: Since the user cancelled this, we could conceivably just 478 478 // throw it away or make it more clear that it's a user cancel.
+2
src/applications/phortune/query/PhortuneCartSearchEngine.php
··· 31 31 array( 32 32 PhortuneCart::STATUS_PURCHASING, 33 33 PhortuneCart::STATUS_CHARGED, 34 + PhortuneCart::STATUS_HOLD, 35 + PhortuneCart::STATUS_REVIEW, 34 36 PhortuneCart::STATUS_PURCHASED, 35 37 )); 36 38
+62 -3
src/applications/phortune/storage/PhortuneCart.php
··· 8 8 const STATUS_PURCHASING = 'cart:purchasing'; 9 9 const STATUS_CHARGED = 'cart:charged'; 10 10 const STATUS_HOLD = 'cart:hold'; 11 + const STATUS_REVIEW = 'cart:review'; 11 12 const STATUS_PURCHASED = 'cart:purchased'; 12 13 13 14 protected $accountPHID; ··· 59 60 self::STATUS_PURCHASING => pht('Purchasing'), 60 61 self::STATUS_CHARGED => pht('Charged'), 61 62 self::STATUS_HOLD => pht('Hold'), 63 + self::STATUS_REVIEW => pht('Review'), 62 64 self::STATUS_PURCHASED => pht('Purchased'), 63 65 ); 64 66 } ··· 163 165 $this->endReadLocking(); 164 166 $this->saveTransaction(); 165 167 166 - foreach ($this->purchases as $purchase) { 167 - $purchase->getProduct()->didPurchaseProduct($purchase); 168 + // TODO: Perform purchase review. Here, we would apply rules to determine 169 + // whether the charge needs manual review (maybe making the decision via 170 + // Herald, configuration, or by examining provider fraud data). For now, 171 + // always require review. 172 + $needs_review = true; 173 + 174 + if ($needs_review) { 175 + $this->willReviewCart(); 176 + } else { 177 + $this->didReviewCart(); 168 178 } 169 179 170 - $this->setStatus(self::STATUS_PURCHASED)->save(); 180 + return $this; 181 + } 182 + 183 + public function willReviewCart() { 184 + $this->openTransaction(); 185 + $this->beginReadLocking(); 186 + 187 + $copy = clone $this; 188 + $copy->reload(); 189 + 190 + if (($copy->getStatus() !== self::STATUS_CHARGED)) { 191 + throw new Exception( 192 + pht( 193 + 'Cart has wrong status ("%s") to call willReviewCart()!', 194 + $copy->getStatus())); 195 + } 196 + 197 + $this->setStatus(self::STATUS_REVIEW)->save(); 198 + 199 + $this->endReadLocking(); 200 + $this->saveTransaction(); 201 + 202 + // TODO: Notify merchant to review order. 203 + 204 + return $this; 205 + } 206 + 207 + public function didReviewCart() { 208 + $this->openTransaction(); 209 + $this->beginReadLocking(); 210 + 211 + $copy = clone $this; 212 + $copy->reload(); 213 + 214 + if (($copy->getStatus() !== self::STATUS_CHARGED) && 215 + ($copy->getStatus() !== self::STATUS_REVIEW)) { 216 + throw new Exception( 217 + pht( 218 + 'Cart has wrong status ("%s") to call didReviewCart()!', 219 + $copy->getStatus())); 220 + } 221 + 222 + foreach ($this->purchases as $purchase) { 223 + $purchase->getProduct()->didPurchaseProduct($purchase); 224 + } 225 + 226 + $this->setStatus(self::STATUS_PURCHASED)->save(); 227 + 228 + $this->endReadLocking(); 229 + $this->saveTransaction(); 171 230 172 231 return $this; 173 232 }