Add boilerplate scaffolding for Phortune subscriptions

Summary:
Ref T6881. This roughs in the major objects, support classes, and controllers.

  - Show subscriptions on account detail.
  - Browse all account subscriptions.
  - Link to active subsciptions from merchant detail.

Test Plan: Clicked around in the UI. There's no way to create subscriptions yet, so I basically just kicked the tires on this. I probably missed a few things that I'll clean up in followups.

Reviewers: btrahan

Reviewed By: btrahan

Subscribers: epriestley

Maniphest Tasks: T6881

Differential Revision: https://secure.phabricator.com/D11482
This commit is contained in:
epriestley
2015-01-27 14:50:20 -08:00
parent 5b6b2ee850
commit 7720b799e4
12 changed files with 744 additions and 1 deletions

View File

@@ -2804,6 +2804,13 @@ phutil_register_library_map(array(
'PhortunePurchaseQuery' => 'applications/phortune/query/PhortunePurchaseQuery.php',
'PhortuneSchemaSpec' => 'applications/phortune/storage/PhortuneSchemaSpec.php',
'PhortuneStripePaymentProvider' => 'applications/phortune/provider/PhortuneStripePaymentProvider.php',
'PhortuneSubscription' => 'applications/phortune/storage/PhortuneSubscription.php',
'PhortuneSubscriptionImplementation' => 'applications/phortune/subscription/PhortuneSubscriptionImplementation.php',
'PhortuneSubscriptionListController' => 'applications/phortune/controller/PhortuneSubscriptionListController.php',
'PhortuneSubscriptionPHIDType' => 'applications/phortune/phid/PhortuneSubscriptionPHIDType.php',
'PhortuneSubscriptionQuery' => 'applications/phortune/query/PhortuneSubscriptionQuery.php',
'PhortuneSubscriptionSearchEngine' => 'applications/phortune/query/PhortuneSubscriptionSearchEngine.php',
'PhortuneSubscriptionTableView' => 'applications/phortune/view/PhortuneSubscriptionTableView.php',
'PhortuneTestPaymentProvider' => 'applications/phortune/provider/PhortuneTestPaymentProvider.php',
'PhortuneWePayPaymentProvider' => 'applications/phortune/provider/PhortuneWePayPaymentProvider.php',
'PhragmentBrowseController' => 'applications/phragment/controller/PhragmentBrowseController.php',
@@ -6144,6 +6151,15 @@ phutil_register_library_map(array(
'PhortunePurchaseQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
'PhortuneSchemaSpec' => 'PhabricatorConfigSchemaSpec',
'PhortuneStripePaymentProvider' => 'PhortunePaymentProvider',
'PhortuneSubscription' => array(
'PhortuneDAO',
'PhabricatorPolicyInterface',
),
'PhortuneSubscriptionListController' => 'PhortuneController',
'PhortuneSubscriptionPHIDType' => 'PhabricatorPHIDType',
'PhortuneSubscriptionQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
'PhortuneSubscriptionSearchEngine' => 'PhabricatorApplicationSearchEngine',
'PhortuneSubscriptionTableView' => 'AphrontView',
'PhortuneTestPaymentProvider' => 'PhortunePaymentProvider',
'PhortuneWePayPaymentProvider' => 'PhortunePaymentProvider',
'PhragmentBrowseController' => 'PhragmentController',

View File

@@ -45,6 +45,8 @@ final class PhabricatorPhortuneApplication extends PhabricatorApplication {
),
'order/(?:query/(?P<queryKey>[^/]+)/)?'
=> 'PhortuneCartListController',
'subscription/(?:query/(?P<queryKey>[^/]+)/)?'
=> 'PhortuneSubscriptionListController',
'charge/(?:query/(?P<queryKey>[^/]+)/)?'
=> 'PhortuneChargeListController',
),
@@ -77,8 +79,10 @@ final class PhabricatorPhortuneApplication extends PhabricatorApplication {
'merchant/' => array(
'(?:query/(?P<queryKey>[^/]+)/)?' => 'PhortuneMerchantListController',
'edit/(?:(?P<id>\d+)/)?' => 'PhortuneMerchantEditController',
'orders/(?P<merchantID>\d+)/(?:query/(?P<querKey>[^/]+)/)?'
'orders/(?P<merchantID>\d+)/(?:query/(?P<queryKey>[^/]+)/)?'
=> 'PhortuneCartListController',
'subscription/(?P<merchantID>\d+)/(?:query/(?P<queryKey>[^/]+)/)?'
=> 'PhortuneSubscriptionListController',
'(?P<id>\d+)/' => 'PhortuneMerchantViewController',
),
),

View File

@@ -70,6 +70,8 @@ final class PhortuneAccountViewController extends PhortuneController {
$payment_methods = $this->buildPaymentMethodsSection($account);
$purchase_history = $this->buildPurchaseHistorySection($account);
$charge_history = $this->buildChargeHistorySection($account);
$subscriptions = $this->buildSubscriptionsSection($account);
$timeline = $this->buildTransactionTimeline(
$account,
new PhortuneAccountTransactionQuery());
@@ -86,6 +88,7 @@ final class PhortuneAccountViewController extends PhortuneController {
$payment_methods,
$purchase_history,
$charge_history,
$subscriptions,
$timeline,
),
array(
@@ -259,6 +262,39 @@ final class PhortuneAccountViewController extends PhortuneController {
->appendChild($table);
}
private function buildSubscriptionsSection(PhortuneAccount $account) {
$request = $this->getRequest();
$viewer = $request->getUser();
$subscriptions = id(new PhortuneSubscriptionQuery())
->setViewer($viewer)
->withAccountPHIDs(array($account->getPHID()))
->setLimit(10)
->execute();
$subscriptions_uri = $this->getApplicationURI(
$account->getID().'/subscription/');
$table = id(new PhortuneSubscriptionTableView())
->setUser($viewer)
->setSubscriptions($subscriptions);
$header = id(new PHUIHeaderView())
->setHeader(pht('Recent Subscriptions'))
->addActionLink(
id(new PHUIButtonView())
->setTag('a')
->setIcon(
id(new PHUIIconView())
->setIconFont('fa-list'))
->setHref($subscriptions_uri)
->setText(pht('View All Subscriptions')));
return id(new PHUIObjectBoxView())
->setHeader($header)
->appendChild($table);
}
protected function buildApplicationCrumbs() {
$crumbs = parent::buildApplicationCrumbs();

View File

@@ -186,6 +186,14 @@ final class PhortuneMerchantViewController
->setDisabled(!$can_edit)
->setWorkflow(!$can_edit));
$view->addAction(
id(new PhabricatorActionView())
->setName(pht('View Subscriptions'))
->setIcon('fa-moon-o')
->setHref($this->getApplicationURI("merchant/subscription/{$id}/"))
->setDisabled(!$can_edit)
->setWorkflow(!$can_edit));
return $view;
}

View File

@@ -0,0 +1,110 @@
<?php
final class PhortuneSubscriptionListController
extends PhortuneController {
private $accountID;
private $merchantID;
private $queryKey;
private $merchant;
private $account;
public function willProcessRequest(array $data) {
$this->merchantID = idx($data, 'merchantID');
$this->accountID = idx($data, 'accountID');
$this->queryKey = idx($data, 'queryKey');
}
public function processRequest() {
$request = $this->getRequest();
$viewer = $request->getUser();
$engine = new PhortuneSubscriptionSearchEngine();
if ($this->merchantID) {
$merchant = id(new PhortuneMerchantQuery())
->setViewer($viewer)
->withIDs(array($this->merchantID))
->requireCapabilities(
array(
PhabricatorPolicyCapability::CAN_VIEW,
PhabricatorPolicyCapability::CAN_EDIT,
))
->executeOne();
if (!$merchant) {
return new Aphront404Response();
}
$this->merchant = $merchant;
$engine->setMerchant($merchant);
} else if ($this->accountID) {
$account = id(new PhortuneAccountQuery())
->setViewer($viewer)
->withIDs(array($this->accountID))
->requireCapabilities(
array(
PhabricatorPolicyCapability::CAN_VIEW,
PhabricatorPolicyCapability::CAN_EDIT,
))
->executeOne();
if (!$account) {
return new Aphront404Response();
}
$this->account = $account;
$engine->setAccount($account);
} else {
return new Aphront404Response();
}
$controller = id(new PhabricatorApplicationSearchController())
->setQueryKey($this->queryKey)
->setSearchEngine($engine)
->setNavigation($this->buildSideNavView());
return $this->delegateToController($controller);
}
public function buildSideNavView() {
$viewer = $this->getRequest()->getUser();
$nav = new AphrontSideNavFilterView();
$nav->setBaseURI(new PhutilURI($this->getApplicationURI()));
id(new PhortuneSubscriptionSearchEngine())
->setViewer($viewer)
->addNavigationItems($nav->getMenu());
$nav->selectFilter(null);
return $nav;
}
protected function buildApplicationCrumbs() {
$crumbs = parent::buildApplicationCrumbs();
$merchant = $this->merchant;
if ($merchant) {
$id = $merchant->getID();
$crumbs->addTextCrumb(
$merchant->getName(),
$this->getApplicationURI("merchant/{$id}/"));
$crumbs->addTextCrumb(
pht('Subscriptions'),
$this->getApplicationURI("merchant/subscriptions/{$id}/"));
}
$account = $this->account;
if ($account) {
$id = $account->getID();
$crumbs->addTextCrumb(
$account->getName(),
$this->getApplicationURI("{$id}/"));
$crumbs->addTextCrumb(
pht('Subscriptions'),
$this->getApplicationURI("{$id}/subscription/"));
}
return $crumbs;
}
}

View File

@@ -0,0 +1,38 @@
<?php
final class PhortuneSubscriptionPHIDType extends PhabricatorPHIDType {
const TYPECONST = 'PSUB';
public function getTypeName() {
return pht('Phortune Subscription');
}
public function newObject() {
return new PhortuneSubscription();
}
protected function buildQueryForObjects(
PhabricatorObjectQuery $query,
array $phids) {
return id(new PhortuneSubscriptionQuery())
->withPHIDs($phids);
}
public function loadHandles(
PhabricatorHandleQuery $query,
array $handles,
array $objects) {
foreach ($handles as $phid => $handle) {
$subscription = $objects[$phid];
$id = $subscription->getID();
// TODO: Flesh this out.
}
}
}

View File

@@ -0,0 +1,152 @@
<?php
final class PhortuneSubscriptionQuery
extends PhabricatorCursorPagedPolicyAwareQuery {
private $ids;
private $phids;
private $accountPHIDs;
private $merchantPHIDs;
private $statuses;
public function withIDs(array $ids) {
$this->ids = $ids;
return $this;
}
public function withPHIDs(array $phids) {
$this->phids = $phids;
return $this;
}
public function withAccountPHIDs(array $account_phids) {
$this->accountPHIDs = $account_phids;
return $this;
}
public function withMerchantPHIDs(array $merchant_phids) {
$this->merchantPHIDs = $merchant_phids;
return $this;
}
public function withStatuses(array $statuses) {
$this->statuses = $statuses;
return $this;
}
protected function loadPage() {
$table = new PhortuneSubscription();
$conn = $table->establishConnection('r');
$rows = queryfx_all(
$conn,
'SELECT subscription.* FROM %T subscription %Q %Q %Q',
$table->getTableName(),
$this->buildWhereClause($conn),
$this->buildOrderClause($conn),
$this->buildLimitClause($conn));
return $table->loadAllFromArray($rows);
}
protected function willFilterPage(array $subscriptions) {
$accounts = id(new PhortuneAccountQuery())
->setViewer($this->getViewer())
->withPHIDs(mpull($subscriptions, 'getAccountPHID'))
->execute();
$accounts = mpull($accounts, null, 'getPHID');
foreach ($subscriptions as $key => $subscription) {
$account = idx($accounts, $subscription->getAccountPHID());
if (!$account) {
unset($subscriptions[$key]);
continue;
}
$subscription->attachAccount($account);
}
$merchants = id(new PhortuneMerchantQuery())
->setViewer($this->getViewer())
->withPHIDs(mpull($subscriptions, 'getMerchantPHID'))
->execute();
$merchants = mpull($merchants, null, 'getPHID');
foreach ($subscriptions as $key => $subscription) {
$merchant = idx($merchants, $subscription->getMerchantPHID());
if (!$merchant) {
unset($subscriptions[$key]);
continue;
}
$subscription->attachMerchant($merchant);
}
$implementations = array();
$subscription_map = mgroup($subscriptions, 'getSubscriptionClass');
foreach ($subscription_map as $class => $class_subscriptions) {
$sub = newv($class, array());
$implementations += $sub->loadImplementationsForSubscriptions(
$this->getViewer(),
$class_subscriptions);
}
foreach ($subscriptions as $key => $subscription) {
$implementation = idx($implementations, $key);
if (!$implementation) {
unset($subscriptions[$key]);
continue;
}
$subscription->attachImplementation($implementation);
}
return $subscriptions;
}
private function buildWhereClause(AphrontDatabaseConnection $conn) {
$where = array();
$where[] = $this->buildPagingClause($conn);
if ($this->ids !== null) {
$where[] = qsprintf(
$conn,
'subscription.id IN (%Ld)',
$this->ids);
}
if ($this->phids !== null) {
$where[] = qsprintf(
$conn,
'subscription.phid IN (%Ls)',
$this->phids);
}
if ($this->accountPHIDs !== null) {
$where[] = qsprintf(
$conn,
'subscription.accountPHID IN (%Ls)',
$this->accountPHIDs);
}
if ($this->merchantPHIDs !== null) {
$where[] = qsprintf(
$conn,
'subscription.merchantPHID IN (%Ls)',
$this->merchantPHIDs);
}
if ($this->statuses !== null) {
$where[] = qsprintf(
$conn,
'subscription.status IN (%Ls)',
$this->statuses);
}
return $this->formatWhereClause($where);
}
public function getQueryApplicationClass() {
return 'PhabricatorPhortuneApplication';
}
}

View File

@@ -0,0 +1,154 @@
<?php
final class PhortuneSubscriptionSearchEngine
extends PhabricatorApplicationSearchEngine {
private $merchant;
private $account;
public function setAccount(PhortuneAccount $account) {
$this->account = $account;
return $this;
}
public function getAccount() {
return $this->account;
}
public function setMerchant(PhortuneMerchant $merchant) {
$this->merchant = $merchant;
return $this;
}
public function getMerchant() {
return $this->merchant;
}
public function getResultTypeDescription() {
return pht('Phortune Subscriptions');
}
public function buildSavedQueryFromRequest(AphrontRequest $request) {
$saved = new PhabricatorSavedQuery();
return $saved;
}
public function buildQueryFromSavedQuery(PhabricatorSavedQuery $saved) {
$query = id(new PhortuneSubscriptionQuery());
$viewer = $this->requireViewer();
$merchant = $this->getMerchant();
$account = $this->getAccount();
if ($merchant) {
$can_edit = PhabricatorPolicyFilter::hasCapability(
$viewer,
$merchant,
PhabricatorPolicyCapability::CAN_EDIT);
if (!$can_edit) {
throw new Exception(
pht(
'You can not query subscriptions for a merchant you do not '.
'control.'));
}
$query->withMerchantPHIDs(array($merchant->getPHID()));
} else if ($account) {
$can_edit = PhabricatorPolicyFilter::hasCapability(
$viewer,
$account,
PhabricatorPolicyCapability::CAN_EDIT);
if (!$can_edit) {
throw new Exception(
pht(
'You can not query subscriptions for an account you are not '.
'a member of.'));
}
$query->withAccountPHIDs(array($account->getPHID()));
} else {
$accounts = id(new PhortuneAccountQuery())
->withMemberPHIDs(array($viewer->getPHID()))
->execute();
if ($accounts) {
$query->withAccountPHIDs(mpull($accounts, 'getPHID'));
} else {
throw new Exception(pht('You have no accounts!'));
}
}
return $query;
}
public function buildSearchForm(
AphrontFormView $form,
PhabricatorSavedQuery $saved_query) {}
protected function getURI($path) {
$merchant = $this->getMerchant();
$account = $this->getAccount();
if ($merchant) {
return '/phortune/merchant/'.$merchant->getID().'/subscription/'.$path;
} else if ($account) {
return '/phortune/'.$account->getID().'/subscription/';
} else {
return '/phortune/subscription/'.$path;
}
}
protected function getBuiltinQueryNames() {
$names = array(
'all' => pht('All Subscriptions'),
);
return $names;
}
public function buildSavedQueryFromBuiltin($query_key) {
$query = $this->newSavedQuery();
$query->setQueryKey($query_key);
switch ($query_key) {
case 'all':
return $query;
}
return parent::buildSavedQueryFromBuiltin($query_key);
}
protected function getRequiredHandlePHIDsForResultList(
array $subscriptions,
PhabricatorSavedQuery $query) {
$phids = array();
foreach ($subscriptions as $subscription) {
$phids[] = $subscription->getPHID();
$phids[] = $subscription->getMerchantPHID();
$phids[] = $subscription->getAuthorPHID();
}
return $phids;
}
protected function renderResultList(
array $subscriptions,
PhabricatorSavedQuery $query,
array $handles) {
assert_instances_of($subscriptions, 'PhortuneSubscription');
$viewer = $this->requireViewer();
$table = id(new PhortuneSubscriptionTableView())
->setUser($viewer)
->setSubscriptions($subscriptions);
$merchant = $this->getMerchant();
if ($merchant) {
$header = pht('Subscriptions for %s', $merchant->getName());
} else {
$header = pht('Your Subscriptions');
}
return id(new PHUIObjectBoxView())
->setHeaderText($header)
->appendChild($table);
}
}

View File

@@ -0,0 +1,131 @@
<?php
/**
* A subscription bills users regularly.
*/
final class PhortuneSubscription extends PhortuneDAO
implements PhabricatorPolicyInterface {
const STATUS_ACTIVE = 'active';
const STATUS_CANCELLED = 'cancelled';
protected $accountPHID;
protected $merchantPHID;
protected $triggerPHID;
protected $authorPHID;
protected $subscriptionClassKey;
protected $subscriptionClass;
protected $subscriptionRefKey;
protected $subscriptionRef;
protected $status;
protected $metadata = array();
private $merchant = self::ATTACHABLE;
private $account = self::ATTACHABLE;
private $implementation = self::ATTACHABLE;
private $trigger = self::ATTACHABLE;
protected function getConfiguration() {
return array(
self::CONFIG_AUX_PHID => true,
self::CONFIG_SERIALIZATION => array(
'metadata' => self::SERIALIZATION_JSON,
),
self::CONFIG_COLUMN_SCHEMA => array(
'subscriptionClassKey' => 'bytes12',
'subscriptionClass' => 'text128',
'subscriptionRefKey' => 'bytes12',
'subscriptionRef' => 'text128',
'status' => 'text32',
),
self::CONFIG_KEY_SCHEMA => array(
'key_subscription' => array(
'columns' => array('subscriptionClassKey', 'subscriptionRefKey'),
'unique' => true,
),
'key_account' => array(
'columns' => array('accountPHID'),
),
'key_merchant' => array(
'columns' => array('merchantPHID'),
),
),
) + parent::getConfiguration();
}
public function generatePHID() {
return PhabricatorPHID::generateNewPHID(
PhortuneSubscriptionPHIDType::TYPECONST);
}
public static function initializeNewSubscription() {
return id(new PhortuneSubscription());
}
public function attachImplementation(
PhortuneSubscriptionImplementation $impl) {
$this->implementation = $impl;
}
public function getImplementation() {
return $this->assertAttached($this->implementation);
}
public function save() {
$this->subscriptionClassKey = PhabricatorHash::digestForIndex(
$this->subscriptionClass);
$this->subscriptionRefKey = PhabricatorHash::digestForIndex(
$this->subscriptionRef);
return parent::save();
}
/* -( PhabricatorPolicyInterface )----------------------------------------- */
public function getCapabilities() {
return array(
PhabricatorPolicyCapability::CAN_VIEW,
PhabricatorPolicyCapability::CAN_EDIT,
);
}
public function getPolicy($capability) {
// NOTE: Both view and edit use the account's edit policy. We punch a hole
// through this for merchants, below.
return $this
->getAccount()
->getPolicy(PhabricatorPolicyCapability::CAN_EDIT);
}
public function hasAutomaticCapability($capability, PhabricatorUser $viewer) {
if ($this->getAccount()->hasAutomaticCapability($capability, $viewer)) {
return true;
}
// If the viewer controls the merchant this subscription bills to, they can
// view the subscription.
if ($capability == PhabricatorPolicyCapability::CAN_VIEW) {
$can_admin = PhabricatorPolicyFilter::hasCapability(
$viewer,
$this->getMerchant(),
PhabricatorPolicyCapability::CAN_EDIT);
if ($can_admin) {
return true;
}
}
return false;
}
public function describeAutomaticCapability($capability) {
return array(
pht('Subscriptions inherit the policies of the associated account.'),
pht(
'The merchant you are subscribed with can review and manage the '.
'subscription.'),
);
}
}

View File

@@ -0,0 +1,18 @@
<?php
abstract class PhortuneSubscriptionImplementation {
abstract public function loadImplementationsForRefs(
PhabricatorUser $viewer,
array $refs);
abstract public function getRef();
abstract public function getName(PhortuneSubscription $subscription);
protected function getContentSource() {
return PhabricatorContentSource::newForSource(
PhabricatorContentSource::SOURCE_PHORTUNE,
array());
}
}

View File

@@ -0,0 +1,56 @@
<?php
final class PhortuneSubscriptionTableView extends AphrontView {
private $subscriptions;
private $handles;
public function setHandles(array $handles) {
$this->handles = $handles;
return $this;
}
public function getHandles() {
return $this->handles;
}
public function setSubscriptions(array $subscriptions) {
$this->subscriptions = $subscriptions;
return $this;
}
public function getSubscriptions() {
return $this->subscriptions;
}
public function render() {
$subscriptions = $this->getSubscriptions();
$handles = $this->getHandles();
$viewer = $this->getUser();
$rows = array();
$rowc = array();
foreach ($subscriptions as $subscription) {
$subscription_link = $handles[$subscription->getPHID()]->renderLink();
$rows[] = array(
$subscription->getID(),
phabricator_datetime($subscription->getDateCreated(), $viewer),
);
}
$table = id(new AphrontTableView($rows))
->setHeaders(
array(
pht('ID'),
pht('Created'),
))
->setColumnClasses(
array(
'',
'right',
));
return $table;
}
}