Commit 20163484 authored by epriestley's avatar epriestley
Browse files

Make Phortune payment methods transaction-oriented and always support "Add Payment Method"

Summary:
Depends on D20718. Ref T13366. Ref T13367.

  - Phortune payment methods currently do not use transactions; update them.
  - Give them a proper view page with a transaction log.
  - Add an "Add Payment Method" button which always works.
  - Show which subscriptions a payment method is associated with.
  - Get rid of the "Active" status indicator since we now treat "disabled" as "removed", to align with user expectation/intent.
  - Swap out of some of the super weird div-form-button UI into the new "big, clickable" UI for choice dialogs among a small number of options on a single dimension.

Test Plan:
  - As a mechant-authority and account-authority, created payment methods from carts, subscriptions, and accounts. Edited and viewed payment methods.

Subscribers: PHID-OPKG-gm6ozazyms6q6i22gyam

Maniphest Tasks: T13367, T13366

Differential Revision: https://secure.phabricator.com/D20719
parent c4e0ac4d
......@@ -92,7 +92,7 @@ return array(
'rsrc/css/application/pholio/pholio.css' => '88ef5ef1',
'rsrc/css/application/phortune/phortune-credit-card-form.css' => '3b9868a8',
'rsrc/css/application/phortune/phortune-invoice.css' => '4436b241',
'rsrc/css/application/phortune/phortune.css' => '12e8251a',
'rsrc/css/application/phortune/phortune.css' => '508a1a5e',
'rsrc/css/application/phrequent/phrequent.css' => 'bd79cc67',
'rsrc/css/application/phriction/phriction-document-css.css' => '03380da0',
'rsrc/css/application/policy/policy-edit.css' => '8794e2ed',
......@@ -810,7 +810,7 @@ return array(
'pholio-inline-comments-css' => '722b48c2',
'phortune-credit-card-form' => 'd12d214f',
'phortune-credit-card-form-css' => '3b9868a8',
'phortune-css' => '12e8251a',
'phortune-css' => '508a1a5e',
'phortune-invoice-css' => '4436b241',
'phrequent-css' => 'bd79cc67',
'phriction-document-css' => '03380da0',
......
CREATE TABLE {$NAMESPACE}_phortune.phortune_paymentmethodtransaction (
id INT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
phid VARBINARY(64) NOT NULL,
authorPHID VARBINARY(64) NOT NULL,
objectPHID VARBINARY(64) NOT NULL,
viewPolicy VARBINARY(64) NOT NULL,
editPolicy VARBINARY(64) NOT NULL,
commentPHID VARBINARY(64) DEFAULT NULL,
commentVersion INT UNSIGNED NOT NULL,
transactionType VARCHAR(32) NOT NULL,
oldValue LONGTEXT NOT NULL,
newValue LONGTEXT NOT NULL,
contentSource LONGTEXT NOT NULL,
metadata LONGTEXT NOT NULL,
dateCreated INT UNSIGNED NOT NULL,
dateModified INT UNSIGNED NOT NULL,
UNIQUE KEY `key_phid` (`phid`),
KEY `key_object` (`objectPHID`)
) ENGINE=InnoDB DEFAULT CHARSET={$CHARSET} COLLATE {$COLLATE_TEXT};
......@@ -5249,7 +5249,8 @@ phutil_register_library_map(array(
'PhortuneAccountOrdersController' => 'applications/phortune/controller/account/PhortuneAccountOrdersController.php',
'PhortuneAccountOverviewController' => 'applications/phortune/controller/account/PhortuneAccountOverviewController.php',
'PhortuneAccountPHIDType' => 'applications/phortune/phid/PhortuneAccountPHIDType.php',
'PhortuneAccountPaymentMethodsController' => 'applications/phortune/controller/account/PhortuneAccountPaymentMethodsController.php',
'PhortuneAccountPaymentMethodListController' => 'applications/phortune/controller/account/PhortuneAccountPaymentMethodListController.php',
'PhortuneAccountPaymentMethodViewController' => 'applications/phortune/controller/account/PhortuneAccountPaymentMethodViewController.php',
'PhortuneAccountProfileController' => 'applications/phortune/controller/account/PhortuneAccountProfileController.php',
'PhortuneAccountQuery' => 'applications/phortune/query/PhortuneAccountQuery.php',
'PhortuneAccountSubscriptionController' => 'applications/phortune/controller/account/PhortuneAccountSubscriptionController.php',
......@@ -5325,12 +5326,18 @@ phutil_register_library_map(array(
'PhortuneOrderTableView' => 'applications/phortune/view/PhortuneOrderTableView.php',
'PhortunePayPalPaymentProvider' => 'applications/phortune/provider/PhortunePayPalPaymentProvider.php',
'PhortunePaymentMethod' => 'applications/phortune/storage/PhortunePaymentMethod.php',
'PhortunePaymentMethodCreateController' => 'applications/phortune/controller/payment/PhortunePaymentMethodCreateController.php',
'PhortunePaymentMethodDisableController' => 'applications/phortune/controller/payment/PhortunePaymentMethodDisableController.php',
'PhortunePaymentMethodEditController' => 'applications/phortune/controller/payment/PhortunePaymentMethodEditController.php',
'PhortunePaymentMethodCreateController' => 'applications/phortune/controller/paymentmethod/PhortunePaymentMethodCreateController.php',
'PhortunePaymentMethodDisableController' => 'applications/phortune/controller/paymentmethod/PhortunePaymentMethodDisableController.php',
'PhortunePaymentMethodEditController' => 'applications/phortune/controller/paymentmethod/PhortunePaymentMethodEditController.php',
'PhortunePaymentMethodEditor' => 'applications/phortune/editor/PhortunePaymentMethodEditor.php',
'PhortunePaymentMethodNameTransaction' => 'applications/phortune/xaction/paymentmethod/PhortunePaymentMethodNameTransaction.php',
'PhortunePaymentMethodPHIDType' => 'applications/phortune/phid/PhortunePaymentMethodPHIDType.php',
'PhortunePaymentMethodPolicyCodex' => 'applications/phortune/codex/PhortunePaymentMethodPolicyCodex.php',
'PhortunePaymentMethodQuery' => 'applications/phortune/query/PhortunePaymentMethodQuery.php',
'PhortunePaymentMethodStatusTransaction' => 'applications/phortune/xaction/paymentmethod/PhortunePaymentMethodStatusTransaction.php',
'PhortunePaymentMethodTransaction' => 'applications/phortune/storage/PhortunePaymentMethodTransaction.php',
'PhortunePaymentMethodTransactionQuery' => 'applications/phortune/query/PhortunePaymentMethodTransactionQuery.php',
'PhortunePaymentMethodTransactionType' => 'applications/phortune/xaction/paymentmethod/PhortunePaymentMethodTransactionType.php',
'PhortunePaymentProvider' => 'applications/phortune/provider/PhortunePaymentProvider.php',
'PhortunePaymentProviderConfig' => 'applications/phortune/storage/PhortunePaymentProviderConfig.php',
'PhortunePaymentProviderConfigEditor' => 'applications/phortune/editor/PhortunePaymentProviderConfigEditor.php',
......@@ -11805,7 +11812,8 @@ phutil_register_library_map(array(
'PhortuneAccountOrdersController' => 'PhortuneAccountProfileController',
'PhortuneAccountOverviewController' => 'PhortuneAccountProfileController',
'PhortuneAccountPHIDType' => 'PhabricatorPHIDType',
'PhortuneAccountPaymentMethodsController' => 'PhortuneAccountProfileController',
'PhortuneAccountPaymentMethodListController' => 'PhortuneAccountProfileController',
'PhortuneAccountPaymentMethodViewController' => 'PhortuneAccountController',
'PhortuneAccountProfileController' => 'PhortuneAccountController',
'PhortuneAccountQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
'PhortuneAccountSubscriptionController' => 'PhortuneAccountProfileController',
......@@ -11896,13 +11904,20 @@ phutil_register_library_map(array(
'PhabricatorPolicyInterface',
'PhabricatorExtendedPolicyInterface',
'PhabricatorPolicyCodexInterface',
'PhabricatorApplicationTransactionInterface',
),
'PhortunePaymentMethodCreateController' => 'PhortuneController',
'PhortunePaymentMethodDisableController' => 'PhortuneController',
'PhortunePaymentMethodEditController' => 'PhortuneController',
'PhortunePaymentMethodEditor' => 'PhabricatorApplicationTransactionEditor',
'PhortunePaymentMethodNameTransaction' => 'PhortunePaymentMethodTransactionType',
'PhortunePaymentMethodPHIDType' => 'PhabricatorPHIDType',
'PhortunePaymentMethodPolicyCodex' => 'PhabricatorPolicyCodex',
'PhortunePaymentMethodQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
'PhortunePaymentMethodStatusTransaction' => 'PhortunePaymentMethodTransactionType',
'PhortunePaymentMethodTransaction' => 'PhabricatorModularTransaction',
'PhortunePaymentMethodTransactionQuery' => 'PhabricatorApplicationTransactionQuery',
'PhortunePaymentMethodTransactionType' => 'PhabricatorModularTransactionType',
'PhortunePaymentProvider' => 'Phobject',
'PhortunePaymentProviderConfig' => array(
'PhortuneDAO',
......
......@@ -72,7 +72,10 @@ final class PhabricatorPhortuneApplication extends PhabricatorApplication {
'(?P<accountID>\d+)/' => array(
'details/' => 'PhortuneAccountDetailsController',
'methods/' => 'PhortuneAccountPaymentMethodsController',
'methods/' => array(
'' => 'PhortuneAccountPaymentMethodListController',
'(?P<id>\d+)/' => 'PhortuneAccountPaymentMethodViewController',
),
'orders/' => 'PhortuneAccountOrdersController',
'charges/' => 'PhortuneAccountChargesController',
'subscriptions/' => 'PhortuneAccountSubscriptionController',
......
......@@ -23,6 +23,10 @@ abstract class PhortuneAccountController
abstract protected function shouldRequireAccountEditCapability();
abstract protected function handleAccountRequest(AphrontRequest $request);
private function hasAccount() {
return (bool)$this->account;
}
final protected function getAccount() {
if ($this->account === null) {
throw new Exception(
......@@ -37,8 +41,10 @@ abstract class PhortuneAccountController
protected function buildApplicationCrumbs() {
$crumbs = parent::buildApplicationCrumbs();
$account = $this->getAccount();
if ($account) {
// If we hit a policy exception, we can make it here without finding
// an account.
if ($this->hasAccount()) {
$account = $this->getAccount();
$crumbs->addTextCrumb($account->getName(), $account->getURI());
}
......
<?php
final class PhortuneAccountPaymentMethodsController
final class PhortuneAccountPaymentMethodListController
extends PhortuneAccountProfileController {
protected function shouldRequireAccountEditCapability() {
......@@ -46,15 +46,17 @@ final class PhortuneAccountPaymentMethodsController
$id = $account->getID();
// TODO: Allow adding a card here directly
$add = id(new PHUIButtonView())
->setTag('a')
->setText(pht('New Payment Method'))
->setText(pht('Add Payment Method'))
->setIcon('fa-plus')
->setHref($this->getApplicationURI("{$id}/card/new/"));
->setHref($this->getApplicationURI("{$id}/card/new/"))
->setDisabled(!$can_edit)
->setWorkflow(!$can_edit);
$header = id(new PHUIHeaderView())
->setHeader(pht('Payment Methods'));
->setHeader(pht('Payment Methods'))
->addActionLink($add);
$list = id(new PHUIObjectItemListView())
->setUser($viewer)
......@@ -74,39 +76,14 @@ final class PhortuneAccountPaymentMethodsController
foreach ($methods as $method) {
$id = $method->getID();
$item = new PHUIObjectItemView();
$item->setHeader($method->getFullDisplayName());
switch ($method->getStatus()) {
case PhortunePaymentMethod::STATUS_ACTIVE:
$item->setStatusIcon('fa-check green');
$disable_uri = $this->getApplicationURI('card/'.$id.'/disable/');
$item->addAction(
id(new PHUIListItemView())
->setIcon('fa-times')
->setHref($disable_uri)
->setDisabled(!$can_edit)
->setWorkflow(true));
break;
case PhortunePaymentMethod::STATUS_DISABLED:
$item->setStatusIcon('fa-ban lightbluetext');
$item->setDisabled(true);
break;
}
$item = id(new PHUIObjectItemView())
->setObjectName($method->getObjectName())
->setHeader($method->getFullDisplayName())
->setHref($method->getURI());
$provider = $method->buildPaymentProvider();
$item->addAttribute($provider->getPaymentMethodProviderDescription());
$edit_uri = $this->getApplicationURI('card/'.$id.'/edit/');
$item->addAction(
id(new PHUIListItemView())
->setIcon('fa-pencil')
->setHref($edit_uri)
->setDisabled(!$can_edit)
->setWorkflow(!$can_edit));
$list->addItem($item);
}
......
<?php
final class PhortuneAccountPaymentMethodViewController
extends PhortuneAccountController {
protected function shouldRequireAccountEditCapability() {
return false;
}
protected function handleAccountRequest(AphrontRequest $request) {
$viewer = $this->getViewer();
$account = $this->getAccount();
$method = id(new PhortunePaymentMethodQuery())
->setViewer($viewer)
->withAccountPHIDs(array($account->getPHID()))
->withIDs(array($request->getURIData('id')))
->withStatuses(
array(
PhortunePaymentMethod::STATUS_ACTIVE,
))
->executeOne();
if (!$method) {
return new Aphront404Response();
}
$crumbs = $this->buildApplicationCrumbs()
->addTextCrumb(pht('Payment Methods'), $account->getPaymentMethodsURI())
->addTextCrumb($method->getObjectName())
->setBorder(true);
$header = id(new PHUIHeaderView())
->setHeader($method->getFullDisplayName());
$details = $this->newDetailsView($method);
$timeline = $this->buildTransactionTimeline(
$method,
new PhortunePaymentMethodTransactionQuery());
$timeline->setShouldTerminate(true);
$autopay = $this->newAutopayView($method);
$curtain = $this->buildCurtainView($method);
$view = id(new PHUITwoColumnView())
->setHeader($header)
->setCurtain($curtain)
->setMainColumn(
array(
$details,
$autopay,
$timeline,
));
return $this->newPage()
->setTitle($method->getObjectName())
->setCrumbs($crumbs)
->appendChild($view);
}
private function buildCurtainView(PhortunePaymentMethod $method) {
$viewer = $this->getViewer();
$can_edit = PhabricatorPolicyFilter::hasCapability(
$viewer,
$method,
PhabricatorPolicyCapability::CAN_EDIT);
$edit_uri = $this->getApplicationURI(
urisprintf(
'card/%d/edit/',
$method->getID()));
$remove_uri = $this->getApplicationURI(
urisprintf(
'card/%d/disable/',
$method->getID()));
$curtain = $this->newCurtainView($method);
$curtain->addAction(
id(new PhabricatorActionView())
->setName(pht('Edit Payment Method'))
->setIcon('fa-pencil')
->setHref($edit_uri)
->setDisabled(!$can_edit)
->setWorkflow(!$can_edit));
$curtain->addAction(
id(new PhabricatorActionView())
->setName(pht('Remove Payment Method'))
->setIcon('fa-times')
->setHref($remove_uri)
->setDisabled(!$can_edit)
->setWorkflow(true));
return $curtain;
}
private function newDetailsView(PhortunePaymentMethod $method) {
$viewer = $this->getViewer();
$merchant_phid = $method->getMerchantPHID();
$handles = $viewer->loadHandles(
array(
$merchant_phid,
));
$view = id(new PHUIPropertyListView())
->setUser($viewer);
if (strlen($method->getName())) {
$view->addProperty(pht('Name'), $method->getDisplayName());
}
$view->addProperty(pht('Summary'), $method->getSummary());
$view->addProperty(pht('Expires'), $method->getDisplayExpires());
$view->addProperty(
pht('Merchant'),
$handles[$merchant_phid]->renderLink());
return id(new PHUIObjectBoxView())
->setHeaderText(pht('Payment Method Details'))
->setBackground(PHUIObjectBoxView::BLUE_PROPERTY)
->addPropertyList($view);
}
private function newAutopayView(PhortunePaymentMethod $method) {
$viewer = $this->getViewer();
$subscriptions = id(new PhortuneSubscriptionQuery())
->setViewer($viewer)
->withPaymentMethodPHIDs(array($method->getPHID()))
->execute();
$table = id(new PhortuneSubscriptionTableView())
->setViewer($viewer)
->setSubscriptions($subscriptions)
->newTableView();
$table->setNoDataString(
pht(
'This payment method is not the default payment method for '.
'any subscriptions.'));
return id(new PHUIObjectBoxView())
->setHeaderText(pht('Autopay Subscriptions'))
->setBackground(PHUIObjectBoxView::BLUE_PROPERTY)
->setTable($table);
}
}
......@@ -47,11 +47,8 @@ final class PhortuneAccountSubscriptionController
->setLimit(25)
->execute();
$handles = $this->loadViewerHandles(mpull($subscriptions, 'getPHID'));
$table = id(new PhortuneSubscriptionTableView())
->setUser($viewer)
->setHandles($handles)
->setSubscriptions($subscriptions);
$header = id(new PHUIHeaderView())
......
......@@ -5,34 +5,147 @@ final class PhortunePaymentMethodCreateController
public function handleRequest(AphrontRequest $request) {
$viewer = $request->getViewer();
$account_id = $request->getURIData('accountID');
$account_id = $request->getURIData('accountID');
$account = id(new PhortuneAccountQuery())
->setViewer($viewer)
->withIDs(array($account_id))
->requireCapabilities(
array(
PhabricatorPolicyCapability::CAN_VIEW,
PhabricatorPolicyCapability::CAN_EDIT,
))
->executeOne();
if (!$account) {
return new Aphront404Response();
}
$account_id = $account->getID();
$merchant = id(new PhortuneMerchantQuery())
->setViewer($viewer)
->withIDs(array($request->getInt('merchantID')))
->executeOne();
if (!$merchant) {
return new Aphront404Response();
}
$cart_id = $request->getInt('cartID');
$subscription_id = $request->getInt('subscriptionID');
$merchant_id = $request->getInt('merchantID');
if ($cart_id) {
$cancel_uri = $this->getApplicationURI("cart/{$cart_id}/checkout/");
$cart = id(new PhortuneCartQuery())
->setViewer($viewer)
->withAccountPHIDs(array($account->getPHID()))
->withIDs(array($cart_id))
->executeOne();
if (!$cart) {
return new Aphront404Response();
}
$subscription_phid = $cart->getSubscriptionPHID();
if ($subscription_phid) {
$subscription = id(new PhortuneSubscriptionQuery())
->setViewer($viewer)
->withAccountPHIDs(array($account->getPHID()))
->withPHIDs(array($subscription_phid))
->executeOne();
if (!$subscription) {
return new Aphront404Response();
}
} else {
$subscription = null;
}
$merchant = $cart->getMerchant();
$cart_id = $cart->getID();
$subscription_id = null;
$merchant_id = null;
$next_uri = $cart->getCheckoutURI();
} else if ($subscription_id) {
$cancel_uri = $this->getApplicationURI(
"{$account_id}/subscription/edit/{$subscription_id}/");
$subscription = id(new PhortuneSubscriptionQuery())
->setViewer($viewer)
->withAccountPHIDs(array($account->getPHID()))
->withIDs(array($subscription_id))
->executeOne();
if (!$subscription) {
return new Aphront404Response();
}
$cart = null;
$merchant = $subscription->getMerchant();
$cart_id = null;
$subscription_id = $subscription->getID();
$merchant_id = null;
$next_uri = $subscription->getURI();
} else if ($merchant_id) {
$merchant_phids = $account->getMerchantPHIDs();
if ($merchant_phids) {
$merchant = id(new PhortuneMerchantQuery())
->setViewer($viewer)
->withIDs(array($merchant_id))
->withPHIDs($merchant_phids)
->executeOne();
} else {
$merchant = null;
}
if (!$merchant) {
return new Aphront404Response();
}
$cart = null;
$subscription = null;
$cart_id = null;
$subscription_id = null;
$merchant_id = $merchant->getID();
$next_uri = $account->getPaymentMethodsURI();
} else {
$cancel_uri = $this->getApplicationURI($account->getID().'/');
$next_uri = $account->getPaymentMethodsURI();
$merchant_phids = $account->getMerchantPHIDs();
if ($merchant_phids) {
$merchants = id(new PhortuneMerchantQuery())
->setViewer($viewer)
->withPHIDs($merchant_phids)
->needProfileImage(true)
->execute();
} else {
$merchants = array();
}
if (!$merchants) {
return $this->newDialog()
->setTitle(pht('No Merchants'))
->appendParagraph(
pht(
'You have not established a relationship with any merchants '.
'yet. Create an order or subscription before adding payment '.
'methods.'))
->addCancelButton($next_uri);
}
// If there's more than one merchant, ask the user to pick which one they
// want to pay. If there's only one, just pick it for them.
if (count($merchants) > 1) {
$menu = $this->newMerchantMenu($merchants);
$form = id(new AphrontFormView())
->appendInstructions(
pht(
'Choose the merchant you want to pay.'));
return $this->newDialog()
->setTitle(pht('Choose a Merchant'))
->appendForm($form)
->appendChild($menu)
->addCancelButton($next_uri);
}
$cart = null;
$subscription = null;
$merchant = head($merchants);
$cart_id = null;
$subscription_id = null;
$merchant_id = $merchant->getID();
}
$providers = $this->loadCreatePaymentMethodProvidersForMerchant($merchant);
......@@ -43,34 +156,39 @@ final class PhortunePaymentMethodCreateController
'methods.'));
}
if (count($providers) == 1) {
// If there's only one provider, always choose it.
$provider_id = head_key($providers);
} else {
$provider_id = $request->getInt('providerID');
if (empty($providers[$provider_id])) {
$choices = array();
foreach ($providers as $provider) {
$choices[] = $this->renderSelectProvider($provider);
}
$state_params = array(
'cartID' => $cart_id,
'subscriptionID' => $subscription_id,
'merchantID' => $merchant_id,
);
$state_params = array_filter($state_params);
$content = phutil_tag(
'div',
array(
'class' => 'phortune-payment-method-list',
),