diff --git a/resources/sql/autopatches/20141006.phortunemerchant.sql b/resources/sql/autopatches/20141006.phortunemerchant.sql new file mode 100644 --- /dev/null +++ b/resources/sql/autopatches/20141006.phortunemerchant.sql @@ -0,0 +1,10 @@ +CREATE TABLE {$NAMESPACE}_phortune.phortune_merchant ( + id INT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY, + phid VARCHAR(64) NOT NULL COLLATE utf8_bin, + name VARCHAR(255) NOT NULL COLLATE utf8_bin, + viewPolicy VARCHAR(64) NOT NULL COLLATE utf8_bin, + editPolicy VARCHAR(64) NOT NULL COLLATE utf8_bin, + dateCreated INT UNSIGNED NOT NULL, + dateModified INT UNSIGNED NOT NULL, + UNIQUE KEY `key_phid` (phid) +) ENGINE=InnoDB, COLLATE=utf8_bin; diff --git a/resources/sql/autopatches/20141006.phortunemerchantx.sql b/resources/sql/autopatches/20141006.phortunemerchantx.sql new file mode 100644 --- /dev/null +++ b/resources/sql/autopatches/20141006.phortunemerchantx.sql @@ -0,0 +1,19 @@ +CREATE TABLE {$NAMESPACE}_phortune.phortune_merchanttransaction ( + id INT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY, + phid VARCHAR(64) COLLATE utf8_bin NOT NULL, + authorPHID VARCHAR(64) COLLATE utf8_bin NOT NULL, + objectPHID VARCHAR(64) COLLATE utf8_bin NOT NULL, + viewPolicy VARCHAR(64) COLLATE utf8_bin NOT NULL, + editPolicy VARCHAR(64) COLLATE utf8_bin NOT NULL, + commentPHID VARCHAR(64) COLLATE utf8_bin DEFAULT NULL, + commentVersion INT UNSIGNED NOT NULL, + transactionType VARCHAR(32) COLLATE utf8_bin NOT NULL, + oldValue LONGTEXT COLLATE utf8_bin NOT NULL, + newValue LONGTEXT COLLATE utf8_bin NOT NULL, + contentSource LONGTEXT COLLATE utf8_bin NOT NULL, + metadata LONGTEXT COLLATE utf8_bin NOT NULL, + dateCreated INT UNSIGNED NOT NULL, + dateModified INT UNSIGNED NOT NULL, + UNIQUE KEY `key_phid` (`phid`), + KEY `key_object` (`objectPHID`) +) ENGINE=InnoDB, COLLATE utf8_general_ci; diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php --- a/src/__phutil_library_map__.php +++ b/src/__phutil_library_map__.php @@ -2573,6 +2573,18 @@ 'PhortuneDAO' => 'applications/phortune/storage/PhortuneDAO.php', 'PhortuneErrCode' => 'applications/phortune/constants/PhortuneErrCode.php', 'PhortuneLandingController' => 'applications/phortune/controller/PhortuneLandingController.php', + 'PhortuneMerchant' => 'applications/phortune/storage/PhortuneMerchant.php', + 'PhortuneMerchantCapability' => 'applications/phortune/capability/PhortuneMerchantCapability.php', + 'PhortuneMerchantController' => 'applications/phortune/controller/PhortuneMerchantController.php', + 'PhortuneMerchantEditController' => 'applications/phortune/controller/PhortuneMerchantEditController.php', + 'PhortuneMerchantEditor' => 'applications/phortune/editor/PhortuneMerchantEditor.php', + 'PhortuneMerchantListController' => 'applications/phortune/controller/PhortuneMerchantListController.php', + 'PhortuneMerchantPHIDType' => 'applications/phortune/phid/PhortuneMerchantPHIDType.php', + 'PhortuneMerchantQuery' => 'applications/phortune/query/PhortuneMerchantQuery.php', + 'PhortuneMerchantSearchEngine' => 'applications/phortune/query/PhortuneMerchantSearchEngine.php', + 'PhortuneMerchantTransaction' => 'applications/phortune/storage/PhortuneMerchantTransaction.php', + 'PhortuneMerchantTransactionQuery' => 'applications/phortune/query/PhortuneMerchantTransactionQuery.php', + 'PhortuneMerchantViewController' => 'applications/phortune/controller/PhortuneMerchantViewController.php', 'PhortuneMonthYearExpiryControl' => 'applications/phortune/control/PhortuneMonthYearExpiryControl.php', 'PhortuneMultiplePaymentProvidersException' => 'applications/phortune/exception/PhortuneMultiplePaymentProvidersException.php', 'PhortuneNoPaymentProviderException' => 'applications/phortune/exception/PhortuneNoPaymentProviderException.php', @@ -5608,6 +5620,21 @@ 'PhortuneDAO' => 'PhabricatorLiskDAO', 'PhortuneErrCode' => 'PhortuneConstants', 'PhortuneLandingController' => 'PhortuneController', + 'PhortuneMerchant' => array( + 'PhortuneDAO', + 'PhabricatorPolicyInterface', + ), + 'PhortuneMerchantCapability' => 'PhabricatorPolicyCapability', + 'PhortuneMerchantController' => 'PhortuneController', + 'PhortuneMerchantEditController' => 'PhortuneMerchantController', + 'PhortuneMerchantEditor' => 'PhabricatorApplicationTransactionEditor', + 'PhortuneMerchantListController' => 'PhortuneMerchantController', + 'PhortuneMerchantPHIDType' => 'PhabricatorPHIDType', + 'PhortuneMerchantQuery' => 'PhabricatorCursorPagedPolicyAwareQuery', + 'PhortuneMerchantSearchEngine' => 'PhabricatorApplicationSearchEngine', + 'PhortuneMerchantTransaction' => 'PhabricatorApplicationTransaction', + 'PhortuneMerchantTransactionQuery' => 'PhabricatorApplicationTransactionQuery', + 'PhortuneMerchantViewController' => 'PhortuneMerchantController', 'PhortuneMonthYearExpiryControl' => 'AphrontFormControl', 'PhortuneMultiplePaymentProvidersException' => 'Exception', 'PhortuneNoPaymentProviderException' => 'Exception', diff --git a/src/applications/phortune/application/PhabricatorPhortuneApplication.php b/src/applications/phortune/application/PhabricatorPhortuneApplication.php --- a/src/applications/phortune/application/PhabricatorPhortuneApplication.php +++ b/src/applications/phortune/application/PhabricatorPhortuneApplication.php @@ -60,6 +60,20 @@ ), 'provider/(?P[^/]+)/(?P[^/]+)/' => 'PhortuneProviderController', + 'merchant/' => array( + '(?:query/(?P[^/]+)/)?' => 'PhortuneMerchantListController', + 'edit/(?:(?P\d+)/)?' => 'PhortuneMerchantEditController', + '(?P\d+)/' => 'PhortuneMerchantViewController', + ), + ), + ); + } + + protected function getCustomCapabilities() { + return array( + PhortuneMerchantCapability::CAPABILITY => array( + 'caption' => pht('Merchant accounts can receive payments.'), + 'default' => PhabricatorPolicies::POLICY_ADMIN, ), ); } diff --git a/src/applications/phortune/capability/PhortuneMerchantCapability.php b/src/applications/phortune/capability/PhortuneMerchantCapability.php new file mode 100644 --- /dev/null +++ b/src/applications/phortune/capability/PhortuneMerchantCapability.php @@ -0,0 +1,17 @@ +addTextCrumb( + pht('Merchants'), + $this->getApplicationURI('merchant/')); + return $crumbs; + } +} diff --git a/src/applications/phortune/controller/PhortuneMerchantEditController.php b/src/applications/phortune/controller/PhortuneMerchantEditController.php new file mode 100644 --- /dev/null +++ b/src/applications/phortune/controller/PhortuneMerchantEditController.php @@ -0,0 +1,155 @@ +id = idx($data, 'id'); + } + + public function processRequest() { + $request = $this->getRequest(); + $viewer = $request->getUser(); + + if ($this->id) { + $merchant = id(new PhortuneMerchantQuery()) + ->setViewer($viewer) + ->withIDs(array($this->id)) + ->requireCapabilities( + array( + PhabricatorPolicyCapability::CAN_VIEW, + PhabricatorPolicyCapability::CAN_EDIT, + )) + ->executeOne(); + if (!$merchant) { + return new Aphront404Response(); + } + $is_new = false; + } else { + $this->requireApplicationCapability( + PhortuneMerchantCapability::CAPABILITY); + + $merchant = PhortuneMerchant::initializeNewMerchant($viewer); + $is_new = true; + } + + if ($is_new) { + $title = pht('Create Merchant'); + $button_text = pht('Create Merchant'); + $cancel_uri = $this->getApplicationURI('merchant/'); + } else { + $title = pht( + 'Edit Merchant %d %s', + $merchant->getID(), + $merchant->getName()); + $button_text = pht('Save Changes'); + $cancel_uri = $this->getApplicationURI( + '/merchant/'.$merchant->getID().'/'); + } + + $e_name = true; + $v_name = $merchant->getName(); + + $validation_exception = null; + if ($request->isFormPost()) { + $v_name = $request->getStr('name'); + $v_view = $request->getStr('viewPolicy'); + $v_edit = $request->getStr('editPolicy'); + + $type_name = PhortuneMerchantTransaction::TYPE_NAME; + $type_view = PhabricatorTransactions::TYPE_VIEW_POLICY; + $type_edit = PhabricatorTransactions::TYPE_EDIT_POLICY; + + $xactions = array(); + + $xactions[] = id(new PhortuneMerchantTransaction()) + ->setTransactionType($type_name) + ->setNewValue($v_name); + + $xactions[] = id(new PhortuneMerchantTransaction()) + ->setTransactionType($type_view) + ->setNewValue($v_view); + + $xactions[] = id(new PhortuneMerchantTransaction()) + ->setTransactionType($type_edit) + ->setNewValue($v_edit); + + $editor = id(new PhortuneMerchantEditor()) + ->setActor($viewer) + ->setContentSourceFromRequest($request) + ->setContinueOnNoEffect(true); + + try { + $editor->applyTransactions($merchant, $xactions); + + $id = $merchant->getID(); + $merchant_uri = $this->getApplicationURI("merchant/{$id}/"); + return id(new AphrontRedirectResponse())->setURI($merchant_uri); + } catch (PhabricatorApplicationTransactionValidationException $ex) { + $validation_exception = $ex; + + $e_name = $ex->getShortMessage($type_name); + + $merchant->setViewPolicy($v_view); + $merchant->setEditPolicy($v_edit); + } + } + + $policies = id(new PhabricatorPolicyQuery()) + ->setViewer($viewer) + ->setObject($merchant) + ->execute(); + + $form = id(new AphrontFormView()) + ->setUser($viewer) + ->appendChild( + id(new AphrontFormTextControl()) + ->setName('name') + ->setLabel(pht('Name')) + ->setValue($v_name) + ->setError($e_name)) + ->appendChild( + id(new AphrontFormPolicyControl()) + ->setName('viewPolicy') + ->setPolicyObject($merchant) + ->setCapability(PhabricatorPolicyCapability::CAN_VIEW) + ->setPolicies($policies)) + ->appendChild( + id(new AphrontFormPolicyControl()) + ->setName('editPolicy') + ->setPolicyObject($merchant) + ->setCapability(PhabricatorPolicyCapability::CAN_EDIT) + ->setPolicies($policies)) + ->appendChild( + id(new AphrontFormSubmitControl()) + ->setValue($button_text) + ->addCancelButton($cancel_uri)); + + $crumbs = $this->buildApplicationCrumbs(); + if ($is_new) { + $crumbs->addTextCrumb(pht('Create Merchant')); + } else { + $crumbs->addTextCrumb( + pht('Merchant %d', $merchant->getID()), + $this->getApplicationURI('/merchant/'.$merchant->getID().'/')); + $crumbs->addTextCrumb(pht('Edit')); + } + + $box = id(new PHUIObjectBoxView()) + ->setValidationException($validation_exception) + ->setHeaderText($title) + ->appendChild($form); + + return $this->buildApplicationPage( + array( + $crumbs, + $box, + ), + array( + 'title' => $title, + )); + } + +} diff --git a/src/applications/phortune/controller/PhortuneMerchantListController.php b/src/applications/phortune/controller/PhortuneMerchantListController.php new file mode 100644 --- /dev/null +++ b/src/applications/phortune/controller/PhortuneMerchantListController.php @@ -0,0 +1,58 @@ +queryKey = idx($data, 'queryKey'); + } + + public function processRequest() { + $request = $this->getRequest(); + $controller = id(new PhabricatorApplicationSearchController($request)) + ->setQueryKey($this->queryKey) + ->setSearchEngine(new PhortuneMerchantSearchEngine()) + ->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 PhortuneMerchantSearchEngine()) + ->setViewer($viewer) + ->addNavigationItems($nav->getMenu()); + + $nav->selectFilter(null); + + return $nav; + } + + public function buildApplicationCrumbs() { + $crumbs = parent::buildApplicationCrumbs(); + + $can_create = $this->hasApplicationCapability( + PhortuneMerchantCapability::CAPABILITY); + + $crumbs->addAction( + id(new PHUIListItemView()) + ->setName(pht('Create Merchant')) + ->setHref($this->getApplicationURI('merchant/edit/')) + ->setIcon('fa-plus-square') + ->setWorkflow(!$can_create) + ->setDisabled(!$can_create)); + + return $crumbs; + } + +} diff --git a/src/applications/phortune/controller/PhortuneMerchantViewController.php b/src/applications/phortune/controller/PhortuneMerchantViewController.php new file mode 100644 --- /dev/null +++ b/src/applications/phortune/controller/PhortuneMerchantViewController.php @@ -0,0 +1,101 @@ +id = $data['id']; + } + + public function processRequest() { + $request = $this->getRequest(); + $viewer = $request->getUser(); + + $merchant = id(new PhortuneMerchantQuery()) + ->setViewer($viewer) + ->withIDs(array($this->id)) + ->executeOne(); + if (!$merchant) { + return new Aphront404Response(); + } + + $crumbs = $this->buildApplicationCrumbs(); + $crumbs->addTextCrumb(pht('Merchant %d', $merchant->getID())); + + $title = pht( + 'Merchant %d %s', + $merchant->getID(), + $merchant->getName()); + + $header = id(new PHUIHeaderView()) + ->setObjectName(pht('Merchant %d', $merchant->getID())) + ->setHeader($merchant->getName()) + ->setUser($viewer) + ->setPolicyObject($merchant); + + $properties = $this->buildPropertyListView($merchant); + $actions = $this->buildActionListView($merchant); + $properties->setActionList($actions); + + $box = id(new PHUIObjectBoxView()) + ->setHeader($header) + ->appendChild($properties); + + $xactions = id(new PhortuneMerchantTransactionQuery()) + ->setViewer($viewer) + ->withObjectPHIDs(array($merchant->getPHID())) + ->execute(); + + $timeline = id(new PhabricatorApplicationTransactionView()) + ->setUser($viewer) + ->setObjectPHID($merchant->getPHID()) + ->setTransactions($xactions); + + return $this->buildApplicationPage( + array( + $crumbs, + $box, + $timeline, + ), + array( + 'title' => $title, + )); + } + + private function buildPropertyListView(PhortuneMerchant $merchant) { + $viewer = $this->getRequest()->getUser(); + + $view = id(new PHUIPropertyListView()) + ->setUser($viewer) + ->setObject($merchant); + + return $view; + } + + private function buildActionListView(PhortuneMerchant $merchant) { + $viewer = $this->getRequest()->getUser(); + $id = $merchant->getID(); + + $can_edit = PhabricatorPolicyFilter::hasCapability( + $viewer, + $merchant, + PhabricatorPolicyCapability::CAN_EDIT); + + $view = id(new PhabricatorActionListView()) + ->setUser($viewer) + ->setObject($merchant); + + $view->addAction( + id(new PhabricatorActionView()) + ->setName(pht('Edit Merchant')) + ->setIcon('fa-pencil') + ->setDisabled(!$can_edit) + ->setWorkflow(!$can_edit) + ->setHref($this->getApplicationURI("merchant/edit/{$id}/"))); + + return $view; + } + +} diff --git a/src/applications/phortune/editor/PhortuneMerchantEditor.php b/src/applications/phortune/editor/PhortuneMerchantEditor.php new file mode 100644 --- /dev/null +++ b/src/applications/phortune/editor/PhortuneMerchantEditor.php @@ -0,0 +1,102 @@ +getTransactionType()) { + case PhortuneMerchantTransaction::TYPE_NAME: + return $object->getName(); + } + + return parent::getCustomTransactionOldValue($object, $xaction); + } + + protected function getCustomTransactionNewValue( + PhabricatorLiskDAO $object, + PhabricatorApplicationTransaction $xaction) { + + switch ($xaction->getTransactionType()) { + case PhortuneMerchantTransaction::TYPE_NAME: + return $xaction->getNewValue(); + } + + return parent::getCustomTransactionNewValue($object, $xaction); + } + + protected function applyCustomInternalTransaction( + PhabricatorLiskDAO $object, + PhabricatorApplicationTransaction $xaction) { + + switch ($xaction->getTransactionType()) { + case PhortuneMerchantTransaction::TYPE_NAME: + $object->setName($xaction->getNewValue()); + return; + } + + return parent::applyCustomInternalTransaction($object, $xaction); + } + + protected function applyCustomExternalTransaction( + PhabricatorLiskDAO $object, + PhabricatorApplicationTransaction $xaction) { + + switch ($xaction->getTransactionType()) { + case PhortuneMerchantTransaction::TYPE_NAME: + return; + } + + return parent::applyCustomExternalTransaction($object, $xaction); + } + + protected function validateTransaction( + PhabricatorLiskDAO $object, + $type, + array $xactions) { + + $errors = parent::validateTransaction($object, $type, $xactions); + + switch ($type) { + case PhortuneMerchantTransaction::TYPE_NAME: + $missing = $this->validateIsEmptyTextField( + $object->getName(), + $xactions); + + if ($missing) { + $error = new PhabricatorApplicationTransactionValidationError( + $type, + pht('Required'), + pht('Merchant name is required.'), + nonempty(last($xactions), null)); + + $error->setIsMissingFieldError(true); + $errors[] = $error; + } + break; + } + + return $errors; + } + + +} diff --git a/src/applications/phortune/phid/PhortuneMerchantPHIDType.php b/src/applications/phortune/phid/PhortuneMerchantPHIDType.php new file mode 100644 --- /dev/null +++ b/src/applications/phortune/phid/PhortuneMerchantPHIDType.php @@ -0,0 +1,38 @@ +withPHIDs($phids); + } + + public function loadHandles( + PhabricatorHandleQuery $query, + array $handles, + array $objects) { + + foreach ($handles as $phid => $handle) { + $merchant = $objects[$phid]; + + $id = $merchant->getID(); + + $handle->setName(pht('Merchant %d', $id)); + $handle->setURI("/phortune/merchant/{$id}/"); + } + } + +} diff --git a/src/applications/phortune/query/PhortuneMerchantQuery.php b/src/applications/phortune/query/PhortuneMerchantQuery.php new file mode 100644 --- /dev/null +++ b/src/applications/phortune/query/PhortuneMerchantQuery.php @@ -0,0 +1,60 @@ +ids = $ids; + return $this; + } + + public function withPHIDs(array $phids) { + $this->phids = $phids; + return $this; + } + + protected function loadPage() { + $table = new PhortuneMerchant(); + $conn = $table->establishConnection('r'); + + $rows = queryfx_all( + $conn, + 'SELECT * FROM %T %Q %Q %Q', + $table->getTableName(), + $this->buildWhereClause($conn), + $this->buildOrderClause($conn), + $this->buildLimitClause($conn)); + + return $table->loadAllFromArray($rows); + } + + private function buildWhereClause(AphrontDatabaseConnection $conn) { + $where = array(); + + if ($this->ids !== null) { + $where[] = qsprintf( + $conn, + 'id IN (%Ld)', + $this->ids); + } + + if ($this->phids !== null) { + $where[] = qsprintf( + $conn, + 'phid IN (%Ls)', + $this->phids); + } + + $where[] = $this->buildPagingClause($conn); + + return $this->formatWhereClause($where); + } + + public function getQueryApplicationClass() { + return 'PhabricatorPhortuneApplication'; + } + +} diff --git a/src/applications/phortune/query/PhortuneMerchantSearchEngine.php b/src/applications/phortune/query/PhortuneMerchantSearchEngine.php new file mode 100644 --- /dev/null +++ b/src/applications/phortune/query/PhortuneMerchantSearchEngine.php @@ -0,0 +1,79 @@ + pht('All Merchants'), + ); + + 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 $merchants, + PhabricatorSavedQuery $query) { + return array(); + } + + protected function renderResultList( + array $merchants, + PhabricatorSavedQuery $query, + array $handles) { + assert_instances_of($merchants, 'PhortuneMerchant'); + + $viewer = $this->requireViewer(); + + $list = new PHUIObjectItemListView(); + $list->setUser($viewer); + foreach ($merchants as $merchant) { + $item = id(new PHUIObjectItemView()) + ->setObjectName(pht('Merchant %d', $merchant->getID())) + ->setHeader($merchant->getName()) + ->setHref('/phortune/merchant/'.$merchant->getID().'/') + ->setObject($merchant); + + $list->addItem($item); + } + + return $list; + } +} diff --git a/src/applications/phortune/query/PhortuneMerchantTransactionQuery.php b/src/applications/phortune/query/PhortuneMerchantTransactionQuery.php new file mode 100644 --- /dev/null +++ b/src/applications/phortune/query/PhortuneMerchantTransactionQuery.php @@ -0,0 +1,10 @@ +setViewPolicy(PhabricatorPolicies::getMostOpenPolicy()) + ->setEditPolicy($actor->getPHID()); + } + + public function getConfiguration() { + return array( + self::CONFIG_AUX_PHID => true, + self::CONFIG_COLUMN_SCHEMA => array( + 'name' => 'text255', + ), + ) + parent::getConfiguration(); + } + + public function generatePHID() { + return PhabricatorPHID::generateNewPHID( + PhortuneMerchantPHIDType::TYPECONST); + } + + +/* -( PhabricatorPolicyInterface )----------------------------------------- */ + + + public function getCapabilities() { + return array( + PhabricatorPolicyCapability::CAN_VIEW, + PhabricatorPolicyCapability::CAN_EDIT, + ); + } + + public function getPolicy($capability) { + switch ($capability) { + case PhabricatorPolicyCapability::CAN_VIEW: + return $this->getViewPolicy(); + case PhabricatorPolicyCapability::CAN_EDIT: + return $this->getEditPolicy(); + } + } + + public function hasAutomaticCapability($capability, PhabricatorUser $viewer) { + return false; + } + + public function describeAutomaticCapability($capability) { + return null; + } + +} diff --git a/src/applications/phortune/storage/PhortuneMerchantTransaction.php b/src/applications/phortune/storage/PhortuneMerchantTransaction.php new file mode 100644 --- /dev/null +++ b/src/applications/phortune/storage/PhortuneMerchantTransaction.php @@ -0,0 +1,45 @@ +getAuthorPHID(); + + $old = $this->getOldValue(); + $new = $this->getNewValue(); + + switch ($this->getTransactionType()) { + case self::TYPE_NAME: + if ($old === null) { + return pht( + '%s created this merchant.', + $this->renderHandleLink($author_phid)); + } else { + return pht( + '%s renamed this merchant from "%s" to "%s".', + $this->renderHandleLink($author_phid), + $old, + $new); + } + break; + } + + return parent::getTitle(); + } + +}