@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.

Show invoices on account information page

Summary:
Ref T6881. This is basically just some UX.

Right now, if we invoice you, you can //technically// pay it but since we don't tell you about it and don't show it in the UI you'd have to guess the ID by manipulating the URI. We should probably be at least a little more aggressive about billing.

In the common case when we generate a cart/order, we don't show it to the user or merchant in Phortune until the user takes a payment action (basically, Phortune doesn't recognize the cart until you actually check out with it). In the current use case in Fund (and other reasonable use cases) an un-acted-upon cart hasn't been ordered yet, and is just a place for the application to store state as it hands off the workflow to Phortune.

Even if we had a real "Shop for physical goods" app, I think the same rule would apply -- the application itself would probably track and show your current cart, but it wouldn't make sense to put it into your order history in Phortune until you actually buy it.

Since invoices from subscriptions are essentially identical to not-yet-ordered-carts, that mean they also did not show up in the UI (although I think this is also desirable).

This change carves out a place for them:

- Add an "invoices" section with unpaid invoices.
- The UI shows that you have unpaid invoices.
- Invoices have a slightly different rendering, inclduing an alluring "Pay Now" button.

Some considerations:

- One thing I'm vaguely thinking about is the possibilty that users may be able to invoice one another directly, eventually. For example, we might invoice a contracting client.
- Considering this, I thought about making these carts have a special status like `STATUS_DUE`, which replaces `STATUS_READY`, or a flag like `isInvoice`.
- However, this approach was pretty involved and made the //billing// logic more complicated, so I backed off. The ultimate approach here puts more of the complexity into the display logic, which feels better to me.
- We might need an `isInvoice` flag eventually, but `subscriptionPHID` is a reasonable stand-in for now.
- The OrderTable serving double duty for rendering subscriptions feels a little muddy, but I think splitting it into two highly-redundant classes would be worse.

Test Plan:
{F279348}

{F279349}

Reviewers: btrahan

Reviewed By: btrahan

Subscribers: epriestley

Maniphest Tasks: T6881

Differential Revision: https://secure.phabricator.com/D11593

+193 -17
+1 -1
src/applications/phortune/cart/PhortuneSubscriptionCart.php
··· 25 25 } 26 26 27 27 public function getName(PhortuneCart $cart) { 28 - return pht('Subscription'); 28 + return $this->getSubscription()->getCartName($cart); 29 29 } 30 30 31 31 public function willCreateCart(
+95 -15
src/applications/phortune/controller/PhortuneAccountViewController.php
··· 2 2 3 3 final class PhortuneAccountViewController extends PhortuneController { 4 4 5 - private $accountID; 6 - 7 - public function willProcessRequest(array $data) { 8 - $this->accountID = $data['accountID']; 9 - } 10 - 11 - public function processRequest() { 12 - $request = $this->getRequest(); 13 - $user = $request->getUser(); 5 + public function handleRequest(AphrontRequest $request) { 6 + $viewer = $this->getViewer(); 14 7 15 8 // TODO: Currently, you must be able to edit an account to view the detail 16 9 // page, because the account must be broadly visible so merchants can ··· 20 13 $can_edit = true; 21 14 22 15 $account = id(new PhortuneAccountQuery()) 23 - ->setViewer($user) 24 - ->withIDs(array($this->accountID)) 16 + ->setViewer($viewer) 17 + ->withIDs(array($request->getURIData('accountID'))) 25 18 ->requireCapabilities( 26 19 array( 27 20 PhabricatorPolicyCapability::CAN_VIEW, ··· 34 27 35 28 $title = $account->getName(); 36 29 30 + $invoices = id(new PhortuneCartQuery()) 31 + ->setViewer($viewer) 32 + ->withAccountPHIDs(array($account->getPHID())) 33 + ->needPurchases(true) 34 + ->withInvoices(true) 35 + ->execute(); 36 + 37 37 $crumbs = $this->buildApplicationCrumbs(); 38 38 $this->addAccountCrumb($crumbs, $account, $link = false); 39 39 ··· 43 43 $edit_uri = $this->getApplicationURI('account/edit/'.$account->getID().'/'); 44 44 45 45 $actions = id(new PhabricatorActionListView()) 46 - ->setUser($user) 46 + ->setUser($viewer) 47 47 ->setObjectURI($request->getRequestURI()) 48 48 ->addAction( 49 49 id(new PhabricatorActionView()) ··· 55 55 56 56 $properties = id(new PHUIPropertyListView()) 57 57 ->setObject($account) 58 - ->setUser($user); 58 + ->setUser($viewer); 59 59 60 60 $this->loadHandles($account->getMemberPHIDs()); 61 61 ··· 63 63 pht('Members'), 64 64 $this->renderHandlesForPHIDs($account->getMemberPHIDs())); 65 65 66 + $status_items = $this->getStatusItemsForAccount($account, $invoices); 67 + $status_view = new PHUIStatusListView(); 68 + foreach ($status_items as $item) { 69 + $status_view->addItem( 70 + id(new PHUIStatusItemView()) 71 + ->setIcon( 72 + idx($item, 'icon'), 73 + idx($item, 'color'), 74 + idx($item, 'label')) 75 + ->setTarget(idx($item, 'target')) 76 + ->setNote(idx($item, 'note'))); 77 + } 78 + $properties->addProperty( 79 + pht('Status'), 80 + $status_view); 81 + 66 82 $properties->setActionList($actions); 67 83 68 - $payment_methods = $this->buildPaymentMethodsSection($account); 84 + $invoices = $this->buildInvoicesSection($account, $invoices); 69 85 $purchase_history = $this->buildPurchaseHistorySection($account); 70 86 $charge_history = $this->buildChargeHistorySection($account); 71 87 $subscriptions = $this->buildSubscriptionsSection($account); 88 + $payment_methods = $this->buildPaymentMethodsSection($account); 72 89 73 90 $timeline = $this->buildTransactionTimeline( 74 91 $account, ··· 83 100 array( 84 101 $crumbs, 85 102 $object_box, 86 - $payment_methods, 103 + $invoices, 87 104 $purchase_history, 88 105 $charge_history, 89 106 $subscriptions, 107 + $payment_methods, 90 108 $timeline, 91 109 ), 92 110 array( ··· 165 183 ->appendChild($list); 166 184 } 167 185 186 + private function buildInvoicesSection( 187 + PhortuneAccount $account, 188 + array $carts) { 189 + 190 + $request = $this->getRequest(); 191 + $viewer = $request->getUser(); 192 + 193 + $phids = array(); 194 + foreach ($carts as $cart) { 195 + $phids[] = $cart->getPHID(); 196 + $phids[] = $cart->getMerchantPHID(); 197 + foreach ($cart->getPurchases() as $purchase) { 198 + $phids[] = $purchase->getPHID(); 199 + } 200 + } 201 + $handles = $this->loadViewerHandles($phids); 202 + 203 + $table = id(new PhortuneOrderTableView()) 204 + ->setNoDataString(pht('You have no unpaid invoices.')) 205 + ->setIsInvoices(true) 206 + ->setUser($viewer) 207 + ->setCarts($carts) 208 + ->setHandles($handles); 209 + 210 + $header = id(new PHUIHeaderView()) 211 + ->setHeader(pht('Invoices Due')); 212 + 213 + return id(new PHUIObjectBoxView()) 214 + ->setHeader($header) 215 + ->appendChild($table); 216 + } 217 + 168 218 private function buildPurchaseHistorySection(PhortuneAccount $account) { 169 219 $request = $this->getRequest(); 170 220 $viewer = $request->getUser(); ··· 306 356 ->setName(pht('Switch Accounts'))); 307 357 308 358 return $crumbs; 359 + } 360 + 361 + private function getStatusItemsForAccount( 362 + PhortuneAccount $account, 363 + array $invoices) { 364 + 365 + assert_instances_of($invoices, 'PhortuneCart'); 366 + 367 + $items = array(); 368 + 369 + if ($invoices) { 370 + $items[] = array( 371 + 'icon' => PHUIStatusItemView::ICON_WARNING, 372 + 'color' => 'yellow', 373 + 'target' => pht('Invoices'), 374 + 'note' => pht('You have %d unpaid invoice(s).', count($invoices)), 375 + ); 376 + } else { 377 + $items[] = array( 378 + 'icon' => PHUIStatusItemView::ICON_ACCEPT, 379 + 'color' => 'green', 380 + 'target' => pht('Invoices'), 381 + 'note' => pht('This account has no unpaid invoices.'), 382 + ); 383 + } 384 + 385 + // TODO: If a payment method has expired or is expiring soon, we should 386 + // add a status check for it. 387 + 388 + return $items; 309 389 } 310 390 311 391 }
+27
src/applications/phortune/query/PhortuneCartQuery.php
··· 9 9 private $merchantPHIDs; 10 10 private $subscriptionPHIDs; 11 11 private $statuses; 12 + private $invoices; 12 13 13 14 private $needPurchases; 14 15 ··· 39 40 40 41 public function withStatuses(array $statuses) { 41 42 $this->statuses = $statuses; 43 + return $this; 44 + } 45 + 46 + 47 + /** 48 + * Include or exclude carts which represent invoices with payments due. 49 + * 50 + * @param bool `true` to select invoices; `false` to exclude invoices. 51 + * @return this 52 + */ 53 + public function withInvoices($invoices) { 54 + $this->invoices = $invoices; 42 55 return $this; 43 56 } 44 57 ··· 176 189 $conn, 177 190 'cart.status IN (%Ls)', 178 191 $this->statuses); 192 + } 193 + 194 + if ($this->invoices !== null) { 195 + if ($this->invoices) { 196 + $where[] = qsprintf( 197 + $conn, 198 + 'cart.status = %s AND cart.subscriptionPHID IS NOT NULL', 199 + PhortuneCart::STATUS_READY); 200 + } else { 201 + $where[] = qsprintf( 202 + $conn, 203 + 'cart.status != %s OR cart.subscriptionPHID IS NULL', 204 + PhortuneCart::STATUS_READY); 205 + } 179 206 } 180 207 181 208 return $this->formatWhereClause($where);
+4
src/applications/phortune/storage/PhortuneSubscription.php
··· 166 166 return $this->getImplementation()->getName($this); 167 167 } 168 168 169 + public function getCartName(PhortuneCart $cart) { 170 + return $this->getImplementation()->getCartName($this, $cart); 171 + } 172 + 169 173 public function getURI() { 170 174 $account_id = $this->getAccount()->getID(); 171 175 $id = $this->getID();
+6
src/applications/phortune/subscription/PhortuneSubscriptionImplementation.php
··· 15 15 array()); 16 16 } 17 17 18 + public function getCartName( 19 + PhortuneSubscription $subscription, 20 + PhortuneCart $cart) { 21 + return pht('Subscription'); 22 + } 23 + 18 24 }
+60 -1
src/applications/phortune/view/PhortuneOrderTableView.php
··· 4 4 5 5 private $carts; 6 6 private $handles; 7 + private $noDataString; 8 + private $isInvoices; 7 9 8 10 public function setHandles(array $handles) { 9 11 $this->handles = $handles; ··· 23 25 return $this->carts; 24 26 } 25 27 28 + public function setIsInvoices($is_invoices) { 29 + $this->isInvoices = $is_invoices; 30 + return $this; 31 + } 32 + 33 + public function getIsInvoices() { 34 + return $this->isInvoices; 35 + } 36 + 37 + public function setNoDataString($no_data_string) { 38 + $this->noDataString = $no_data_string; 39 + return $this; 40 + } 41 + 42 + public function getNoDataString() { 43 + return $this->noDataString; 44 + } 45 + 26 46 public function render() { 27 47 $carts = $this->getCarts(); 28 48 $handles = $this->getHandles(); 29 49 $viewer = $this->getUser(); 50 + 51 + $is_invoices = $this->getIsInvoices(); 30 52 31 53 $rows = array(); 32 54 $rowc = array(); ··· 42 64 $purchase_name = ''; 43 65 } 44 66 67 + if ($is_invoices) { 68 + $merchant_link = $handles[$cart->getMerchantPHID()]->renderLink(); 69 + } else { 70 + $merchant_link = null; 71 + } 72 + 45 73 $rowc[] = ''; 46 74 $rows[] = array( 47 75 $cart->getID(), 76 + $merchant_link, 48 77 phutil_tag( 49 78 'strong', 50 79 array(), ··· 56 85 $cart->getTotalPriceAsCurrency()->formatForDisplay()), 57 86 PhortuneCart::getNameForStatus($cart->getStatus()), 58 87 phabricator_datetime($cart->getDateModified(), $viewer), 88 + phabricator_datetime($cart->getDateCreated(), $viewer), 89 + phutil_tag( 90 + 'a', 91 + array( 92 + 'href' => $cart->getCheckoutURI(), 93 + 'class' => 'small green button', 94 + ), 95 + pht('Pay Now')), 59 96 ); 60 97 foreach ($purchases as $purchase) { 61 98 $id = $purchase->getID(); ··· 65 102 $rowc[] = ''; 66 103 $rows[] = array( 67 104 '', 105 + '', 68 106 $handles[$purchase->getPHID()]->renderLink(), 69 107 $price, 108 + '', 70 109 '', 71 110 '', 111 + '', 72 112 ); 73 113 } 74 114 } 75 115 76 116 $table = id(new AphrontTableView($rows)) 117 + ->setNoDataString($this->getNoDataString()) 77 118 ->setRowClasses($rowc) 78 119 ->setHeaders( 79 120 array( 80 121 pht('ID'), 81 - pht('Order'), 122 + pht('Merchant'), 123 + $is_invoices ? pht('Invoice') : pht('Order'), 82 124 pht('Purchase'), 83 125 pht('Amount'), 84 126 pht('Status'), 85 127 pht('Updated'), 128 + pht('Invoice Date'), 129 + null, 86 130 )) 87 131 ->setColumnClasses( 88 132 array( 89 133 '', 90 134 '', 135 + '', 91 136 'wide', 92 137 'right', 93 138 '', 94 139 'right', 140 + 'right', 141 + '', 142 + )) 143 + ->setColumnVisibility( 144 + array( 145 + true, 146 + $is_invoices, 147 + true, 148 + true, 149 + true, 150 + !$is_invoices, 151 + !$is_invoices, 152 + $is_invoices, 153 + $is_invoices, 95 154 )); 96 155 97 156 return $table;