diff --git a/resources/sql/autopatches/20150124.subs.1.sql b/resources/sql/autopatches/20150124.subs.1.sql new file mode 100644 index 0000000000..07d60d5792 --- /dev/null +++ b/resources/sql/autopatches/20150124.subs.1.sql @@ -0,0 +1,20 @@ +CREATE TABLE {$NAMESPACE}_phortune.phortune_subscription ( + id INT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY, + phid VARBINARY(64) NOT NULL, + accountPHID VARBINARY(64) NOT NULL, + merchantPHID VARBINARY(64) NOT NULL, + triggerPHID VARBINARY(64) NOT NULL, + authorPHID VARBINARY(64) NOT NULL, + subscriptionClassKey BINARY(12) NOT NULL, + subscriptionClass VARCHAR(128) NOT NULL COLLATE {$COLLATE_TEXT}, + subscriptionRefKey BINARY(12) NOT NULL, + subscriptionRef VARCHAR(128) NOT NULL COLLATE {$COLLATE_TEXT}, + status VARCHAR(32) NOT NULL COLLATE {$COLLATE_TEXT}, + metadata LONGTEXT NOT NULL COLLATE {$COLLATE_TEXT}, + dateCreated INT UNSIGNED NOT NULL, + dateModified INT UNSIGNED NOT NULL, + UNIQUE KEY `key_phid` (phid), + UNIQUE KEY `key_subscription` (subscriptionClassKey, subscriptionRefKey), + KEY `key_account` (accountPHID), + KEY `key_merchant` (merchantPHID) +) ENGINE=InnoDB, COLLATE {$COLLATE_TEXT}; diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php index 03e1409b25..b30b54e9b5 100644 --- a/src/__phutil_library_map__.php +++ b/src/__phutil_library_map__.php @@ -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', diff --git a/src/applications/phortune/application/PhabricatorPhortuneApplication.php b/src/applications/phortune/application/PhabricatorPhortuneApplication.php index 54b1ff4d55..c852f18b62 100644 --- a/src/applications/phortune/application/PhabricatorPhortuneApplication.php +++ b/src/applications/phortune/application/PhabricatorPhortuneApplication.php @@ -45,6 +45,8 @@ final class PhabricatorPhortuneApplication extends PhabricatorApplication { ), 'order/(?:query/(?P[^/]+)/)?' => 'PhortuneCartListController', + 'subscription/(?:query/(?P[^/]+)/)?' + => 'PhortuneSubscriptionListController', 'charge/(?:query/(?P[^/]+)/)?' => 'PhortuneChargeListController', ), @@ -77,8 +79,10 @@ final class PhabricatorPhortuneApplication extends PhabricatorApplication { 'merchant/' => array( '(?:query/(?P[^/]+)/)?' => 'PhortuneMerchantListController', 'edit/(?:(?P\d+)/)?' => 'PhortuneMerchantEditController', - 'orders/(?P\d+)/(?:query/(?P[^/]+)/)?' + 'orders/(?P\d+)/(?:query/(?P[^/]+)/)?' => 'PhortuneCartListController', + 'subscription/(?P\d+)/(?:query/(?P[^/]+)/)?' + => 'PhortuneSubscriptionListController', '(?P\d+)/' => 'PhortuneMerchantViewController', ), ), diff --git a/src/applications/phortune/controller/PhortuneAccountViewController.php b/src/applications/phortune/controller/PhortuneAccountViewController.php index fdbc061688..59dc2b76f6 100644 --- a/src/applications/phortune/controller/PhortuneAccountViewController.php +++ b/src/applications/phortune/controller/PhortuneAccountViewController.php @@ -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(); diff --git a/src/applications/phortune/controller/PhortuneMerchantViewController.php b/src/applications/phortune/controller/PhortuneMerchantViewController.php index 0419219f19..cd279084a9 100644 --- a/src/applications/phortune/controller/PhortuneMerchantViewController.php +++ b/src/applications/phortune/controller/PhortuneMerchantViewController.php @@ -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; } diff --git a/src/applications/phortune/controller/PhortuneSubscriptionListController.php b/src/applications/phortune/controller/PhortuneSubscriptionListController.php new file mode 100644 index 0000000000..5adecac3b9 --- /dev/null +++ b/src/applications/phortune/controller/PhortuneSubscriptionListController.php @@ -0,0 +1,110 @@ +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; + } + +} diff --git a/src/applications/phortune/phid/PhortuneSubscriptionPHIDType.php b/src/applications/phortune/phid/PhortuneSubscriptionPHIDType.php new file mode 100644 index 0000000000..553c0bc38c --- /dev/null +++ b/src/applications/phortune/phid/PhortuneSubscriptionPHIDType.php @@ -0,0 +1,38 @@ +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. + + } + } + +} diff --git a/src/applications/phortune/query/PhortuneSubscriptionQuery.php b/src/applications/phortune/query/PhortuneSubscriptionQuery.php new file mode 100644 index 0000000000..3d718df4f5 --- /dev/null +++ b/src/applications/phortune/query/PhortuneSubscriptionQuery.php @@ -0,0 +1,152 @@ +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'; + } + +} diff --git a/src/applications/phortune/query/PhortuneSubscriptionSearchEngine.php b/src/applications/phortune/query/PhortuneSubscriptionSearchEngine.php new file mode 100644 index 0000000000..d8009d8cf4 --- /dev/null +++ b/src/applications/phortune/query/PhortuneSubscriptionSearchEngine.php @@ -0,0 +1,154 @@ +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); + } +} diff --git a/src/applications/phortune/storage/PhortuneSubscription.php b/src/applications/phortune/storage/PhortuneSubscription.php new file mode 100644 index 0000000000..70bf6de409 --- /dev/null +++ b/src/applications/phortune/storage/PhortuneSubscription.php @@ -0,0 +1,131 @@ + 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.'), + ); + } +} diff --git a/src/applications/phortune/subscription/PhortuneSubscriptionImplementation.php b/src/applications/phortune/subscription/PhortuneSubscriptionImplementation.php new file mode 100644 index 0000000000..8376669a2d --- /dev/null +++ b/src/applications/phortune/subscription/PhortuneSubscriptionImplementation.php @@ -0,0 +1,18 @@ +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; + } + +}