From fe5bc764b30c83f0084b7faaf451d047e2c6b121 Mon Sep 17 00:00:00 2001 From: epriestley Date: Thu, 9 Oct 2014 16:59:03 -0700 Subject: [PATCH] Support multiple payment accounts and account switching in Phortune Summary: Ref T2787. Support multiple payment accounts so you can have personal vs company payment accounts. Test Plan: See screenshots. Reviewers: btrahan Reviewed By: btrahan Subscribers: epriestley Maniphest Tasks: T2787 Differential Revision: https://secure.phabricator.com/D10673 --- src/__phutil_library_map__.php | 4 + .../FundInitiativeBackController.php | 24 +++- .../fund/phortune/FundBackerProduct.php | 16 ++- .../PhortuneAccountEditController.php | 122 ++++++++++++++++++ .../PhortuneAccountListController.php | 108 ++++++++++++++++ .../PhortuneAccountViewController.php | 26 +++- .../PhortuneCartCheckoutController.php | 3 +- .../phortune/editor/PhortuneAccountEditor.php | 28 ++++ .../phortune/query/PhortuneAccountQuery.php | 20 +++ .../phortune/storage/PhortuneAccount.php | 2 +- 10 files changed, 338 insertions(+), 15 deletions(-) create mode 100644 src/applications/phortune/controller/PhortuneAccountEditController.php create mode 100644 src/applications/phortune/controller/PhortuneAccountListController.php diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php index a3c91f99fa..1d5ce91ea1 100644 --- a/src/__phutil_library_map__.php +++ b/src/__phutil_library_map__.php @@ -2547,7 +2547,9 @@ phutil_register_library_map(array( 'PholioTransactionView' => 'applications/pholio/view/PholioTransactionView.php', 'PholioUploadedImageView' => 'applications/pholio/view/PholioUploadedImageView.php', 'PhortuneAccount' => 'applications/phortune/storage/PhortuneAccount.php', + 'PhortuneAccountEditController' => 'applications/phortune/controller/PhortuneAccountEditController.php', 'PhortuneAccountEditor' => 'applications/phortune/editor/PhortuneAccountEditor.php', + 'PhortuneAccountListController' => 'applications/phortune/controller/PhortuneAccountListController.php', 'PhortuneAccountPHIDType' => 'applications/phortune/phid/PhortuneAccountPHIDType.php', 'PhortuneAccountQuery' => 'applications/phortune/query/PhortuneAccountQuery.php', 'PhortuneAccountTransaction' => 'applications/phortune/storage/PhortuneAccountTransaction.php', @@ -5601,7 +5603,9 @@ phutil_register_library_map(array( 'PhortuneDAO', 'PhabricatorPolicyInterface', ), + 'PhortuneAccountEditController' => 'PhortuneController', 'PhortuneAccountEditor' => 'PhabricatorApplicationTransactionEditor', + 'PhortuneAccountListController' => 'PhortuneController', 'PhortuneAccountPHIDType' => 'PhabricatorPHIDType', 'PhortuneAccountQuery' => 'PhabricatorCursorPagedPolicyAwareQuery', 'PhortuneAccountTransaction' => 'PhabricatorApplicationTransaction', diff --git a/src/applications/fund/controller/FundInitiativeBackController.php b/src/applications/fund/controller/FundInitiativeBackController.php index 923285cec8..cb4c12ea40 100644 --- a/src/applications/fund/controller/FundInitiativeBackController.php +++ b/src/applications/fund/controller/FundInitiativeBackController.php @@ -39,11 +39,25 @@ final class FundInitiativeBackController ->addCancelButton($initiative_uri); } + $accounts = PhortuneAccountQuery::loadAccountsForUser( + $viewer, + PhabricatorContentSource::newFromRequest($request)); + $v_amount = null; $e_amount = true; + + $v_account = head($accounts)->getPHID(); + $errors = array(); if ($request->isFormPost()) { $v_amount = $request->getStr('amount'); + $v_account = $request->getStr('accountPHID'); + + if (empty($accounts[$v_account])) { + $errors[] = pht('You must specify an account.'); + } else { + $account = $accounts[$v_account]; + } if (!strlen($v_amount)) { $errors[] = pht( @@ -74,10 +88,6 @@ final class FundInitiativeBackController ->withClassAndRef('FundBackerProduct', $initiative->getPHID()) ->executeOne(); - $account = PhortuneAccountQuery::loadActiveAccountForUser( - $viewer, - PhabricatorContentSource::newFromRequest($request)); - $cart_implementation = id(new FundBackerCart()) ->setInitiative($initiative); @@ -110,6 +120,12 @@ final class FundInitiativeBackController $form = id(new AphrontFormView()) ->setUser($viewer) + ->appendChild( + id(new AphrontFormSelectControl()) + ->setName('accountPHID') + ->setLabel(pht('Account')) + ->setValue($v_account) + ->setOptions(mpull($accounts, 'getName', 'getPHID'))) ->appendChild( id(new AphrontFormTextControl()) ->setName('amount') diff --git a/src/applications/fund/phortune/FundBackerProduct.php b/src/applications/fund/phortune/FundBackerProduct.php index 28f3d8c7cc..3ffd149667 100644 --- a/src/applications/fund/phortune/FundBackerProduct.php +++ b/src/applications/fund/phortune/FundBackerProduct.php @@ -79,8 +79,6 @@ final class FundBackerProduct extends PhortuneProductImplementation { public function didPurchaseProduct( PhortuneProduct $product, PhortunePurchase $purchase) { - // TODO: This viewer may be wrong if the purchase completes after a hold - // we should load the backer explicitly. $viewer = $this->getViewer(); $backer = id(new FundBackerQuery()) @@ -91,25 +89,33 @@ final class FundBackerProduct extends PhortuneProductImplementation { throw new Exception(pht('Unable to load FundBacker!')); } + // Load the actual backing user --they may not be the curent viewer if this + // product purchase is completing from a background worker or a merchant + // action. + + $actor = id(new PhabricatorPeopleQuery()) + ->setViewer($viewer) + ->withPHIDs(array($backer->getBackerPHID())) + ->executeOne(); + $xactions = array(); $xactions[] = id(new FundBackerTransaction()) ->setTransactionType(FundBackerTransaction::TYPE_STATUS) ->setNewValue(FundBacker::STATUS_PURCHASED); $editor = id(new FundBackerEditor()) - ->setActor($viewer) + ->setActor($actor) ->setContentSource($this->getContentSource()); $editor->applyTransactions($backer, $xactions); - $xactions = array(); $xactions[] = id(new FundInitiativeTransaction()) ->setTransactionType(FundInitiativeTransaction::TYPE_BACKER) ->setNewValue($backer->getPHID()); $editor = id(new FundInitiativeEditor()) - ->setActor($viewer) + ->setActor($actor) ->setContentSource($this->getContentSource()); $editor->applyTransactions($this->getInitiative(), $xactions); diff --git a/src/applications/phortune/controller/PhortuneAccountEditController.php b/src/applications/phortune/controller/PhortuneAccountEditController.php new file mode 100644 index 0000000000..d5db1b8fcc --- /dev/null +++ b/src/applications/phortune/controller/PhortuneAccountEditController.php @@ -0,0 +1,122 @@ +id = idx($data, 'id'); + } + + public function processRequest() { + $request = $this->getRequest(); + $viewer = $request->getUser(); + + if ($this->id) { + $account = id(new PhortuneAccountQuery()) + ->setViewer($viewer) + ->withIDs(array($this->id)) + ->requireCapabilities( + array( + PhabricatorPolicyCapability::CAN_VIEW, + PhabricatorPolicyCapability::CAN_EDIT, + )) + ->executeOne(); + if (!$account) { + return new Aphront404Response(); + } + $is_new = false; + } else { + $account = PhortuneAccount::initializeNewAccount($viewer); + $is_new = true; + } + + $v_name = $account->getName(); + $e_name = true; + $validation_exception = null; + + if ($request->isFormPost()) { + $v_name = $request->getStr('name'); + + $type_name = PhortuneAccountTransaction::TYPE_NAME; + + $xactions = array(); + $xactions[] = id(new PhortuneAccountTransaction()) + ->setTransactionType($type_name) + ->setNewValue($v_name); + + if ($is_new) { + $xactions[] = id(new PhortuneAccountTransaction()) + ->setTransactionType(PhabricatorTransactions::TYPE_EDGE) + ->setMetadataValue( + 'edge:type', + PhabricatorEdgeConfig::TYPE_ACCOUNT_HAS_MEMBER) + ->setNewValue( + array( + '=' => array($viewer->getPHID() => $viewer->getPHID()), + )); + } + + $editor = id(new PhortuneAccountEditor()) + ->setActor($viewer) + ->setContentSourceFromRequest($request) + ->setContinueOnNoEffect(true); + + try { + $editor->applyTransactions($account, $xactions); + + $account_uri = $this->getApplicationURI($account->getID().'/'); + return id(new AphrontRedirectResponse())->setURI($account_uri); + } catch (PhabricatorApplicationTransactionValidationException $ex) { + $validation_exception = $ex; + $e_name = $ex->getShortMessage($type_name); + } + } + + $crumbs = $this->buildApplicationCrumbs(); + + if ($is_new) { + $cancel_uri = $this->getApplicationURI('account/'); + $crumbs->addTextCrumb(pht('Accounts'), $cancel_uri); + $crumbs->addTextCrumb(pht('Create Account')); + + $title = pht('Create Payment Account'); + $submit_button = pht('Create Account'); + } else { + $cancel_uri = $this->getApplicationURI($account->getID().'/'); + $crumbs->addTextCrumb($account->getName(), $cancel_uri); + $crumbs->addTextCrumb(pht('Edit')); + + $title = pht('Edit %s', $account->getName()); + $submit_button = pht('Save Changes'); + } + + $form = id(new AphrontFormView()) + ->setUser($viewer) + ->appendChild( + id(new AphrontFormTextControl()) + ->setName('name') + ->setLabel(pht('Name')) + ->setValue($v_name) + ->setError($e_name)) + ->appendChild( + id(new AphrontFormSubmitControl()) + ->setValue($submit_button) + ->addCancelButton($cancel_uri)); + + $box = id(new PHUIObjectBoxView()) + ->setHeaderText($title) + ->setValidationException($validation_exception) + ->appendChild($form); + + return $this->buildApplicationPage( + array( + $crumbs, + $box, + ), + array( + 'title' => $title, + )); + } + +} diff --git a/src/applications/phortune/controller/PhortuneAccountListController.php b/src/applications/phortune/controller/PhortuneAccountListController.php new file mode 100644 index 0000000000..9e928f9c46 --- /dev/null +++ b/src/applications/phortune/controller/PhortuneAccountListController.php @@ -0,0 +1,108 @@ +getRequest(); + $viewer = $request->getUser(); + + $accounts = id(new PhortuneAccountQuery()) + ->setViewer($viewer) + ->withMemberPHIDs(array($viewer->getPHID())) + ->requireCapabilities( + array( + PhabricatorPolicyCapability::CAN_VIEW, + PhabricatorPolicyCapability::CAN_EDIT, + )) + ->execute(); + + $merchants = id(new PhortuneMerchantQuery()) + ->setViewer($viewer) + ->requireCapabilities( + array( + PhabricatorPolicyCapability::CAN_VIEW, + PhabricatorPolicyCapability::CAN_EDIT, + )) + ->execute(); + + $title = pht('Accounts'); + + $crumbs = $this->buildApplicationCrumbs(); + $crumbs->addTextCrumb(pht('Accounts')); + + $payment_list = id(new PHUIObjectItemListView()) + ->setUser($viewer) + ->setNoDataString( + pht( + 'You are not a member of any payment accounts. Payment '. + 'accounts are used to make purchases.')); + + foreach ($accounts as $account) { + $item = id(new PHUIObjectItemView()) + ->setObjectName(pht('Account %d', $account->getID())) + ->setHeader($account->getName()) + ->setHref($this->getApplicationURI($account->getID().'/')) + ->setObject($account); + + $payment_list->addItem($item); + } + + $payment_header = id(new PHUIHeaderView()) + ->setHeader(pht('Payment Accounts')) + ->addActionLink( + id(new PHUIButtonView()) + ->setTag('a') + ->setHref($this->getApplicationURI('account/edit/')) + ->setIcon( + id(new PHUIIconView()) + ->setIconFont('fa-plus')) + ->setText(pht('Create Account'))); + + $payment_box = id(new PHUIObjectBoxView()) + ->setHeader($payment_header) + ->appendChild($payment_list); + + $merchant_list = id(new PHUIObjectItemListView()) + ->setUser($viewer) + ->setNoDataString( + pht( + 'You do not control any merchant accounts. Merchant accounts are '. + 'used to receive payments.')); + + foreach ($merchants as $merchant) { + $item = id(new PHUIObjectItemView()) + ->setObjectName(pht('Merchant %d', $merchant->getID())) + ->setHeader($merchant->getName()) + ->setHref($this->getApplicationURI('/merchant/'.$merchant->getID().'/')) + ->setObject($merchant); + + $merchant_list->addItem($item); + } + + $merchant_header = id(new PHUIHeaderView()) + ->setHeader(pht('Merchant Accounts')) + ->addActionLink( + id(new PHUIButtonView()) + ->setTag('a') + ->setHref($this->getApplicationURI('merchant/')) + ->setIcon( + id(new PHUIIconView()) + ->setIconFont('fa-folder-open')) + ->setText(pht('Browse Merchants'))); + + $merchant_box = id(new PHUIObjectBoxView()) + ->setHeader($merchant_header) + ->appendChild($merchant_list); + + return $this->buildApplicationPage( + array( + $crumbs, + $payment_box, + $merchant_box, + ), + array( + 'title' => $title, + )); + } + +} diff --git a/src/applications/phortune/controller/PhortuneAccountViewController.php b/src/applications/phortune/controller/PhortuneAccountViewController.php index 40da7157ce..45d9211c1b 100644 --- a/src/applications/phortune/controller/PhortuneAccountViewController.php +++ b/src/applications/phortune/controller/PhortuneAccountViewController.php @@ -17,6 +17,7 @@ final class PhortuneAccountViewController extends PhortuneController { // process orders but merchants should not be able to see all the details // of an account. Ideally this page should be visible to merchants, too, // just with less information. + $can_edit = true; $account = id(new PhortuneAccountQuery()) ->setViewer($user) @@ -27,7 +28,6 @@ final class PhortuneAccountViewController extends PhortuneController { PhabricatorPolicyCapability::CAN_EDIT, )) ->executeOne(); - if (!$account) { return new Aphront404Response(); } @@ -35,11 +35,15 @@ final class PhortuneAccountViewController extends PhortuneController { $title = $account->getName(); $crumbs = $this->buildApplicationCrumbs(); - $crumbs->addTextCrumb(pht('Account'), $request->getRequestURI()); + $crumbs->addTextCrumb( + $account->getName(), + $request->getRequestURI()); $header = id(new PHUIHeaderView()) ->setHeader($title); + $edit_uri = $this->getApplicationURI('account/edit/'.$account->getID().'/'); + $actions = id(new PhabricatorActionListView()) ->setUser($user) ->setObjectURI($request->getRequestURI()) @@ -47,8 +51,9 @@ final class PhortuneAccountViewController extends PhortuneController { id(new PhabricatorActionView()) ->setName(pht('Edit Account')) ->setIcon('fa-pencil') - ->setHref('#') - ->setDisabled(true)) + ->setHref($edit_uri) + ->setDisabled(!$can_edit) + ->setWorkflow(!$can_edit)) ->addAction( id(new PhabricatorActionView()) ->setName(pht('Edit Members')) @@ -291,4 +296,17 @@ final class PhortuneAccountViewController extends PhortuneController { return $xaction_view; } + protected function buildApplicationCrumbs() { + $crumbs = parent::buildApplicationCrumbs(); + + $crumbs->addAction( + id(new PHUIListItemView()) + ->setIcon('fa-exchange') + ->setHref($this->getApplicationURI('account/')) + ->setName(pht('Switch Accounts'))); + + return $crumbs; + } + + } diff --git a/src/applications/phortune/controller/PhortuneCartCheckoutController.php b/src/applications/phortune/controller/PhortuneCartCheckoutController.php index 8d02ff3759..c0ecc755bb 100644 --- a/src/applications/phortune/controller/PhortuneCartCheckoutController.php +++ b/src/applications/phortune/controller/PhortuneCartCheckoutController.php @@ -114,7 +114,7 @@ final class PhortuneCartCheckoutController ->setHeaderText(pht('Cart Contents')) ->appendChild($cart_table); - $title = pht('Buy Stuff'); + $title = $cart->getName(); if (!$methods) { $method_control = id(new AphrontFormStaticControl()) @@ -210,6 +210,7 @@ final class PhortuneCartCheckoutController ->appendChild($provider_form); $crumbs = $this->buildApplicationCrumbs(); + $crumbs->addTextCrumb(pht('Checkout')); $crumbs->addTextCrumb($title); return $this->buildApplicationPage( diff --git a/src/applications/phortune/editor/PhortuneAccountEditor.php b/src/applications/phortune/editor/PhortuneAccountEditor.php index b64e6e23fc..6e2c91f57b 100644 --- a/src/applications/phortune/editor/PhortuneAccountEditor.php +++ b/src/applications/phortune/editor/PhortuneAccountEditor.php @@ -67,4 +67,32 @@ final class PhortuneAccountEditor return parent::applyCustomExternalTransaction($object, $xaction); } + protected function validateTransaction( + PhabricatorLiskDAO $object, + $type, + array $xactions) { + + $errors = parent::validateTransaction($object, $type, $xactions); + + switch ($type) { + case PhortuneAccountTransaction::TYPE_NAME: + $missing = $this->validateIsEmptyTextField( + $object->getName(), + $xactions); + + if ($missing) { + $error = new PhabricatorApplicationTransactionValidationError( + $type, + pht('Required'), + pht('Account name is required.'), + nonempty(last($xactions), null)); + + $error->setIsMissingFieldError(true); + $errors[] = $error; + } + break; + } + + return $errors; + } } diff --git a/src/applications/phortune/query/PhortuneAccountQuery.php b/src/applications/phortune/query/PhortuneAccountQuery.php index 4e22915467..a0d62c1052 100644 --- a/src/applications/phortune/query/PhortuneAccountQuery.php +++ b/src/applications/phortune/query/PhortuneAccountQuery.php @@ -7,6 +7,26 @@ final class PhortuneAccountQuery private $phids; private $memberPHIDs; + public static function loadAccountsForUser( + PhabricatorUser $user, + PhabricatorContentSource $content_source) { + + $accounts = id(new PhortuneAccountQuery()) + ->setViewer($user) + ->withMemberPHIDs(array($user->getPHID())) + ->execute(); + + if (!$accounts) { + $accounts = array( + PhortuneAccount::createNewAccount($user, $content_source), + ); + } + + $accounts = mpull($accounts, null, 'getPHID'); + + return $accounts; + } + public static function loadActiveAccountForUser( PhabricatorUser $user, PhabricatorContentSource $content_source) { diff --git a/src/applications/phortune/storage/PhortuneAccount.php b/src/applications/phortune/storage/PhortuneAccount.php index c74a1a22ea..3489109308 100644 --- a/src/applications/phortune/storage/PhortuneAccount.php +++ b/src/applications/phortune/storage/PhortuneAccount.php @@ -30,7 +30,7 @@ final class PhortuneAccount extends PhortuneDAO $xactions = array(); $xactions[] = id(new PhortuneAccountTransaction()) ->setTransactionType(PhortuneAccountTransaction::TYPE_NAME) - ->setNewValue(pht('Account (%s)', $actor->getUserName())); + ->setNewValue(pht('Personal Account')); $xactions[] = id(new PhortuneAccountTransaction()) ->setTransactionType(PhabricatorTransactions::TYPE_EDGE)