diff --git a/src/applications/phortune/controller/PhortuneAccountViewController.php b/src/applications/phortune/controller/PhortuneAccountViewController.php index 59dc2b76f6..0bd99bd39b 100644 --- a/src/applications/phortune/controller/PhortuneAccountViewController.php +++ b/src/applications/phortune/controller/PhortuneAccountViewController.php @@ -1,310 +1,313 @@ accountID = $data['accountID']; } public function processRequest() { $request = $this->getRequest(); $user = $request->getUser(); // TODO: Currently, you must be able to edit an account to view the detail // page, because the account must be broadly visible so merchants can // 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) ->withIDs(array($this->accountID)) ->requireCapabilities( array( PhabricatorPolicyCapability::CAN_VIEW, PhabricatorPolicyCapability::CAN_EDIT, )) ->executeOne(); if (!$account) { return new Aphront404Response(); } $title = $account->getName(); $crumbs = $this->buildApplicationCrumbs(); $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()) ->addAction( id(new PhabricatorActionView()) ->setName(pht('Edit Account')) ->setIcon('fa-pencil') ->setHref($edit_uri) ->setDisabled(!$can_edit) ->setWorkflow(!$can_edit)); $properties = id(new PHUIPropertyListView()) ->setObject($account) ->setUser($user); $this->loadHandles($account->getMemberPHIDs()); $properties->addProperty( pht('Members'), $this->renderHandlesForPHIDs($account->getMemberPHIDs())); $properties->setActionList($actions); $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()); $timeline->setShouldTerminate(true); $object_box = id(new PHUIObjectBoxView()) ->setHeader($header) ->addPropertyList($properties); return $this->buildApplicationPage( array( $crumbs, $object_box, $payment_methods, $purchase_history, $charge_history, $subscriptions, $timeline, ), array( 'title' => $title, )); } private function buildPaymentMethodsSection(PhortuneAccount $account) { $request = $this->getRequest(); $viewer = $request->getUser(); $can_edit = PhabricatorPolicyFilter::hasCapability( $viewer, $account, PhabricatorPolicyCapability::CAN_EDIT); $id = $account->getID(); $header = id(new PHUIHeaderView()) ->setHeader(pht('Payment Methods')); $list = id(new PHUIObjectItemListView()) ->setUser($viewer) ->setNoDataString( pht('No payment methods associated with this account.')); $methods = id(new PhortunePaymentMethodQuery()) ->setViewer($viewer) ->withAccountPHIDs(array($account->getPHID())) ->execute(); if ($methods) { $this->loadHandles(mpull($methods, 'getAuthorPHID')); } foreach ($methods as $method) { $id = $method->getID(); $item = new PHUIObjectItemView(); $item->setHeader($method->getFullDisplayName()); switch ($method->getStatus()) { case PhortunePaymentMethod::STATUS_ACTIVE: $item->setBarColor('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->setDisabled(true); break; } $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); } return id(new PHUIObjectBoxView()) ->setHeader($header) ->appendChild($list); } private function buildPurchaseHistorySection(PhortuneAccount $account) { $request = $this->getRequest(); $viewer = $request->getUser(); $carts = id(new PhortuneCartQuery()) ->setViewer($viewer) ->withAccountPHIDs(array($account->getPHID())) ->needPurchases(true) ->withStatuses( array( PhortuneCart::STATUS_PURCHASING, PhortuneCart::STATUS_CHARGED, PhortuneCart::STATUS_HOLD, PhortuneCart::STATUS_REVIEW, PhortuneCart::STATUS_PURCHASED, )) ->setLimit(10) ->execute(); $phids = array(); foreach ($carts as $cart) { $phids[] = $cart->getPHID(); foreach ($cart->getPurchases() as $purchase) { $phids[] = $purchase->getPHID(); } } $handles = $this->loadViewerHandles($phids); $orders_uri = $this->getApplicationURI($account->getID().'/order/'); $table = id(new PhortuneOrderTableView()) ->setUser($viewer) ->setCarts($carts) ->setHandles($handles); $header = id(new PHUIHeaderView()) ->setHeader(pht('Recent Orders')) ->addActionLink( id(new PHUIButtonView()) ->setTag('a') ->setIcon( id(new PHUIIconView()) ->setIconFont('fa-list')) ->setHref($orders_uri) ->setText(pht('View All Orders'))); return id(new PHUIObjectBoxView()) ->setHeader($header) ->appendChild($table); } private function buildChargeHistorySection(PhortuneAccount $account) { $request = $this->getRequest(); $viewer = $request->getUser(); $charges = id(new PhortuneChargeQuery()) ->setViewer($viewer) ->withAccountPHIDs(array($account->getPHID())) ->needCarts(true) ->setLimit(10) ->execute(); $phids = array(); foreach ($charges as $charge) { $phids[] = $charge->getProviderPHID(); $phids[] = $charge->getCartPHID(); $phids[] = $charge->getMerchantPHID(); $phids[] = $charge->getPaymentMethodPHID(); } $handles = $this->loadViewerHandles($phids); $charges_uri = $this->getApplicationURI($account->getID().'/charge/'); $table = id(new PhortuneChargeTableView()) ->setUser($viewer) ->setCharges($charges) ->setHandles($handles); $header = id(new PHUIHeaderView()) ->setHeader(pht('Recent Charges')) ->addActionLink( id(new PHUIButtonView()) ->setTag('a') ->setIcon( id(new PHUIIconView()) ->setIconFont('fa-list')) ->setHref($charges_uri) ->setText(pht('View All Charges'))); return id(new PHUIObjectBoxView()) ->setHeader($header) ->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/'); + $handles = $this->loadViewerHandles(mpull($subscriptions, 'getPHID')); + $table = id(new PhortuneSubscriptionTableView()) ->setUser($viewer) + ->setHandles($handles) ->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(); $crumbs->addAction( id(new PHUIListItemView()) ->setIcon('fa-exchange') ->setHref($this->getApplicationURI('account/')) ->setName(pht('Switch Accounts'))); return $crumbs; } } diff --git a/src/applications/phortune/query/PhortuneSubscriptionQuery.php b/src/applications/phortune/query/PhortuneSubscriptionQuery.php index 3d718df4f5..91c3f07034 100644 --- a/src/applications/phortune/query/PhortuneSubscriptionQuery.php +++ b/src/applications/phortune/query/PhortuneSubscriptionQuery.php @@ -1,152 +1,155 @@ 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( + $impl_objects = $sub->loadImplementationsForRefs( $this->getViewer(), - $class_subscriptions); + mpull($class_subscriptions, 'getSubscriptionRef')); + + $implementations += mpull($impl_objects, null, 'getRef'); } foreach ($subscriptions as $key => $subscription) { - $implementation = idx($implementations, $key); + $ref = $subscription->getSubscriptionRef(); + $implementation = idx($implementations, $ref); 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 index d8009d8cf4..5baa6acc45 100644 --- a/src/applications/phortune/query/PhortuneSubscriptionSearchEngine.php +++ b/src/applications/phortune/query/PhortuneSubscriptionSearchEngine.php @@ -1,154 +1,155 @@ 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) + ->setHandles($handles) ->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 index 70bf6de409..498ef2f9ea 100644 --- a/src/applications/phortune/storage/PhortuneSubscription.php +++ b/src/applications/phortune/storage/PhortuneSubscription.php @@ -1,131 +1,215 @@ 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 static function initializeNewSubscription( + PhortuneAccount $account, + PhortuneMerchant $merchant, + PhabricatorUser $author, + PhortuneSubscriptionImplementation $implementation, + PhabricatorTriggerClock $clock) { + + $trigger = id(new PhabricatorWorkerTrigger()) + ->setClock($clock); + + return id(new PhortuneSubscription()) + ->setStatus(self::STATUS_ACTIVE) + ->setAccountPHID($account->getPHID()) + ->attachAccount($account) + ->setMerchantPHID($merchant->getPHID()) + ->attachMerchant($merchant) + ->setAuthorPHID($author->getPHID()) + ->setSubscriptionClass(get_class($implementation)) + ->setSubscriptionRef($implementation->getRef()) + ->attachImplementation($implementation) + ->attachTrigger($trigger); } public function attachImplementation( PhortuneSubscriptionImplementation $impl) { $this->implementation = $impl; + return $this; } public function getImplementation() { return $this->assertAttached($this->implementation); } + public function attachAccount(PhortuneAccount $account) { + $this->account = $account; + return $this; + } + + public function getAccount() { + return $this->assertAttached($this->account); + } + + public function attachMerchant(PhortuneMerchant $merchant) { + $this->merchant = $merchant; + return $this; + } + + public function getMerchant() { + return $this->assertAttached($this->merchant); + } + + public function attachTrigger(PhabricatorWorkerTrigger $trigger) { + $this->trigger = $trigger; + return $this; + } + + public function getTrigger() { + return $this->assertAttached($this->trigger); + } + public function save() { $this->subscriptionClassKey = PhabricatorHash::digestForIndex( $this->subscriptionClass); $this->subscriptionRefKey = PhabricatorHash::digestForIndex( $this->subscriptionRef); - return parent::save(); + $trigger = $this->getTrigger(); + $is_new = (!$this->getID()); + + $this->openTransaction(); + + // If we're saving this subscription for the first time, we're also + // going to set up the trigger for it. + if ($is_new) { + $trigger_phid = PhabricatorPHID::generateNewPHID( + PhabricatorWorkerTriggerPHIDType::TYPECONST); + $this->setTriggerPHID($trigger_phid); + } + + $result = parent::save(); + + if ($is_new) { + $trigger_action = new PhabricatorScheduleTaskTriggerAction( + array( + 'class' => 'PhortuneSubscriptionWorker', + 'data' => array( + 'subscriptionPHID' => $this->getPHID(), + ), + 'options' => array( + 'objectPHID' => $this->getPHID(), + 'priority' => PhabricatorWorker::PRIORITY_BULK, + ), + )); + + $trigger->setAction($trigger_action); + $trigger->save(); + } + $this->saveTransaction(); + + return $result; + } + + public function getSubscriptionName() { + return $this->getImplementation()->getName($this); } /* -( 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.'), ); } }