From abdaf902395132ad2d5de531e8b86b136e4bcfb7 Mon Sep 17 00:00:00 2001 From: epriestley Date: Thu, 28 Mar 2013 09:13:07 -0700 Subject: [PATCH] Phortune v0.1: products Summary: Ref T2787. A product is the abstract representation of something you can buy or rent/subscribe to. Although the interface isn't locked down yet, this would ultimately be internal/administrative. Products likely have some user-facing skin on top of them: plans would have a purchasing/comparison flow, physical goods would have a storefront, etc., so products don't have any information like descriptions or images, just the data that Phortune needs to correctly bill accounts. Generally, this is very basic for the moment. Test Plan: {F37594} {F37595} {F37596} Reviewers: chad, btrahan Reviewed By: btrahan CC: aran Maniphest Tasks: T2787 Differential Revision: https://secure.phabricator.com/D5441 --- .../sql/patches/20130324.phortuneproduct.sql | 35 ++++ src/__celerity_resource_map__.php | 83 ++++----- src/__phutil_library_map__.php | 21 ++- .../PhabricatorApplicationPhortune.php | 5 + .../PhortuneAccountViewController.php | 15 ++ .../PhortuneProductEditController.php | 162 ++++++++++++++++++ .../PhortuneProductListController.php | 67 ++++++++ .../PhortuneProductViewController.php | 84 +++++++++ .../phortune/editor/PhortuneProductEditor.php | 73 ++++++++ .../phortune/query/PhortuneProductQuery.php | 56 ++++++ .../query/PhortuneProductTransactionQuery.php | 10 ++ .../phortune/storage/PhortuneProduct.php | 48 +++++- .../storage/PhortuneProductTransaction.php | 80 +++++++++ .../phortune/storage/PhortunePurchase.php | 8 + .../phortune/util/PhortuneUtil.php | 34 ++++ ...habricatorApplicationTransactionEditor.php | 9 + .../patch/PhabricatorBuiltinPatchList.php | 4 + 17 files changed, 749 insertions(+), 45 deletions(-) create mode 100644 resources/sql/patches/20130324.phortuneproduct.sql create mode 100644 src/applications/phortune/controller/PhortuneProductEditController.php create mode 100644 src/applications/phortune/controller/PhortuneProductListController.php create mode 100644 src/applications/phortune/controller/PhortuneProductViewController.php create mode 100644 src/applications/phortune/editor/PhortuneProductEditor.php create mode 100644 src/applications/phortune/query/PhortuneProductQuery.php create mode 100644 src/applications/phortune/query/PhortuneProductTransactionQuery.php create mode 100644 src/applications/phortune/storage/PhortuneProductTransaction.php create mode 100644 src/applications/phortune/util/PhortuneUtil.php diff --git a/resources/sql/patches/20130324.phortuneproduct.sql b/resources/sql/patches/20130324.phortuneproduct.sql new file mode 100644 index 0000000000..0f8460c14c --- /dev/null +++ b/resources/sql/patches/20130324.phortuneproduct.sql @@ -0,0 +1,35 @@ +CREATE TABLE {$NAMESPACE}_phortune.phortune_product ( + id INT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY, + phid VARCHAR(64) NOT NULL COLLATE utf8_bin, + productName VARCHAR(255) NOT NULL, + productType VARCHAR(64) NOT NULL COLLATE utf8_bin, + status VARCHAR(64) NOT NULL COLLATE utf8_bin, + priceInCents BIGINT NOT NULL, + billingIntervalInMonths INT UNSIGNED, + trialPeriodInDays INT UNSIGNED, + metadata LONGTEXT NOT NULL COLLATE utf8_bin, + dateCreated INT UNSIGNED NOT NULL, + dateModified INT UNSIGNED NOT NULL, + UNIQUE KEY `key_phid` (phid), + KEY `key_status` (status) +) ENGINE=InnoDB DEFAULT CHARSET=utf8; + +CREATE TABLE {$NAMESPACE}_phortune.phortune_producttransaction ( + id INT UNSIGNED NOT NULL PRIMARY KEY AUTO_INCREMENT, + phid VARCHAR(64) NOT NULL COLLATE utf8_bin, + authorPHID VARCHAR(64) NOT NULL COLLATE utf8_bin, + objectPHID VARCHAR(64) NOT NULL COLLATE utf8_bin, + viewPolicy VARCHAR(64) NOT NULL COLLATE utf8_bin, + editPolicy VARCHAR(64) NOT NULL COLLATE utf8_bin, + commentPHID VARCHAR(64) COLLATE utf8_bin, + commentVersion INT UNSIGNED NOT NULL, + transactionType VARCHAR(32) NOT NULL COLLATE utf8_bin, + oldValue LONGTEXT NOT NULL COLLATE utf8_bin, + newValue LONGTEXT NOT NULL COLLATE utf8_bin, + contentSource LONGTEXT NOT NULL COLLATE utf8_bin, + metadata LONGTEXT NOT NULL COLLATE utf8_bin, + dateCreated INT UNSIGNED NOT NULL, + dateModified INT UNSIGNED NOT NULL, + UNIQUE KEY `key_phid` (phid), + KEY `key_object` (objectPHID) +) ENGINE=InnoDB DEFAULT CHARSET=utf8; diff --git a/src/__celerity_resource_map__.php b/src/__celerity_resource_map__.php index 319d1b3dc1..0eeee08727 100644 --- a/src/__celerity_resource_map__.php +++ b/src/__celerity_resource_map__.php @@ -2138,14 +2138,15 @@ celerity_register_resource_map(array( ), 'javelin-behavior-stripe-payment-form' => array( - 'uri' => '/res/87c7b043/rsrc/js/application/phortune/behavior-stripe-payment-form.js', + 'uri' => '/res/30bcbbb1/rsrc/js/application/phortune/behavior-stripe-payment-form.js', 'type' => 'js', 'requires' => array( 0 => 'javelin-behavior', 1 => 'javelin-dom', 2 => 'javelin-json', - 3 => 'stripe-core', + 3 => 'javelin-workflow', + 4 => 'stripe-core', ), 'disk' => '/rsrc/js/application/phortune/behavior-stripe-payment-form.js', ), @@ -2883,7 +2884,7 @@ celerity_register_resource_map(array( ), 'phabricator-flag-css' => array( - 'uri' => '/res/2eee890a/rsrc/css/application/flag/flag.css', + 'uri' => '/res/237234d7/rsrc/css/application/flag/flag.css', 'type' => 'css', 'requires' => array( @@ -3704,7 +3705,7 @@ celerity_register_resource_map(array( ), array( 'packages' => array( - '20f728ee' => + '4fc2a8f9' => array( 'name' => 'core.pkg.css', 'symbols' => @@ -3746,7 +3747,7 @@ celerity_register_resource_map(array( 34 => 'phabricator-object-item-list-view-css', 35 => 'global-drag-and-drop-css', ), - 'uri' => '/res/pkg/20f728ee/core.pkg.css', + 'uri' => '/res/pkg/4fc2a8f9/core.pkg.css', 'type' => 'css', ), '95ceba95' => @@ -3937,16 +3938,16 @@ celerity_register_resource_map(array( 'reverse' => array( 'aphront-attached-file-view-css' => '6b1fccc6', - 'aphront-dialog-view-css' => '20f728ee', - 'aphront-error-view-css' => '20f728ee', - 'aphront-form-view-css' => '20f728ee', - 'aphront-list-filter-view-css' => '20f728ee', - 'aphront-pager-view-css' => '20f728ee', - 'aphront-panel-view-css' => '20f728ee', - 'aphront-table-view-css' => '20f728ee', - 'aphront-tokenizer-control-css' => '20f728ee', - 'aphront-tooltip-css' => '20f728ee', - 'aphront-typeahead-control-css' => '20f728ee', + 'aphront-dialog-view-css' => '4fc2a8f9', + 'aphront-error-view-css' => '4fc2a8f9', + 'aphront-form-view-css' => '4fc2a8f9', + 'aphront-list-filter-view-css' => '4fc2a8f9', + 'aphront-pager-view-css' => '4fc2a8f9', + 'aphront-panel-view-css' => '4fc2a8f9', + 'aphront-table-view-css' => '4fc2a8f9', + 'aphront-tokenizer-control-css' => '4fc2a8f9', + 'aphront-tooltip-css' => '4fc2a8f9', + 'aphront-typeahead-control-css' => '4fc2a8f9', 'differential-changeset-view-css' => '8aaacd1b', 'differential-core-view-css' => '8aaacd1b', 'differential-inline-comment-editor' => '322728f3', @@ -3960,7 +3961,7 @@ celerity_register_resource_map(array( 'differential-table-of-contents-css' => '8aaacd1b', 'diffusion-commit-view-css' => 'c8ce2d88', 'diffusion-icons-css' => 'c8ce2d88', - 'global-drag-and-drop-css' => '20f728ee', + 'global-drag-and-drop-css' => '4fc2a8f9', 'inline-comment-summary-css' => '8aaacd1b', 'javelin-aphlict' => '95ceba95', 'javelin-behavior' => 'fe22443b', @@ -4032,48 +4033,48 @@ celerity_register_resource_map(array( 'javelin-util' => 'fe22443b', 'javelin-vector' => 'fe22443b', 'javelin-workflow' => 'fe22443b', - 'lightbox-attachment-css' => '20f728ee', + 'lightbox-attachment-css' => '4fc2a8f9', 'maniphest-task-summary-css' => '6b1fccc6', 'maniphest-transaction-detail-css' => '6b1fccc6', 'phabricator-busy' => '95ceba95', 'phabricator-content-source-view-css' => '8aaacd1b', - 'phabricator-core-buttons-css' => '20f728ee', - 'phabricator-core-css' => '20f728ee', - 'phabricator-crumbs-view-css' => '20f728ee', - 'phabricator-directory-css' => '20f728ee', + 'phabricator-core-buttons-css' => '4fc2a8f9', + 'phabricator-core-css' => '4fc2a8f9', + 'phabricator-crumbs-view-css' => '4fc2a8f9', + 'phabricator-directory-css' => '4fc2a8f9', 'phabricator-drag-and-drop-file-upload' => '322728f3', 'phabricator-dropdown-menu' => '95ceba95', 'phabricator-file-upload' => '95ceba95', - 'phabricator-filetree-view-css' => '20f728ee', - 'phabricator-flag-css' => '20f728ee', - 'phabricator-form-view-css' => '20f728ee', - 'phabricator-header-view-css' => '20f728ee', - 'phabricator-jump-nav' => '20f728ee', + 'phabricator-filetree-view-css' => '4fc2a8f9', + 'phabricator-flag-css' => '4fc2a8f9', + 'phabricator-form-view-css' => '4fc2a8f9', + 'phabricator-header-view-css' => '4fc2a8f9', + 'phabricator-jump-nav' => '4fc2a8f9', 'phabricator-keyboard-shortcut' => '95ceba95', 'phabricator-keyboard-shortcut-manager' => '95ceba95', - 'phabricator-main-menu-view' => '20f728ee', + 'phabricator-main-menu-view' => '4fc2a8f9', 'phabricator-menu-item' => '95ceba95', - 'phabricator-nav-view-css' => '20f728ee', + 'phabricator-nav-view-css' => '4fc2a8f9', 'phabricator-notification' => '95ceba95', - 'phabricator-notification-css' => '20f728ee', - 'phabricator-notification-menu-css' => '20f728ee', - 'phabricator-object-item-list-view-css' => '20f728ee', + 'phabricator-notification-css' => '4fc2a8f9', + 'phabricator-notification-menu-css' => '4fc2a8f9', + 'phabricator-object-item-list-view-css' => '4fc2a8f9', 'phabricator-object-selector-css' => '8aaacd1b', 'phabricator-paste-file-upload' => '95ceba95', 'phabricator-prefab' => '95ceba95', 'phabricator-project-tag-css' => '6b1fccc6', - 'phabricator-remarkup-css' => '20f728ee', + 'phabricator-remarkup-css' => '4fc2a8f9', 'phabricator-shaped-request' => '322728f3', - 'phabricator-side-menu-view-css' => '20f728ee', - 'phabricator-standard-page-view' => '20f728ee', + 'phabricator-side-menu-view-css' => '4fc2a8f9', + 'phabricator-standard-page-view' => '4fc2a8f9', 'phabricator-textareautils' => '95ceba95', 'phabricator-tooltip' => '95ceba95', - 'phabricator-transaction-view-css' => '20f728ee', - 'phabricator-zindex-css' => '20f728ee', - 'sprite-apps-large-css' => '20f728ee', - 'sprite-gradient-css' => '20f728ee', - 'sprite-icon-css' => '20f728ee', - 'sprite-menu-css' => '20f728ee', - 'syntax-highlighting-css' => '20f728ee', + 'phabricator-transaction-view-css' => '4fc2a8f9', + 'phabricator-zindex-css' => '4fc2a8f9', + 'sprite-apps-large-css' => '4fc2a8f9', + 'sprite-gradient-css' => '4fc2a8f9', + 'sprite-icon-css' => '4fc2a8f9', + 'sprite-menu-css' => '4fc2a8f9', + 'syntax-highlighting-css' => '4fc2a8f9', ), )); diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php index 0de4836064..ffdcb510af 100644 --- a/src/__phutil_library_map__.php +++ b/src/__phutil_library_map__.php @@ -1546,8 +1546,16 @@ phutil_register_library_map(array( 'PhortunePaymentMethodQuery' => 'applications/phortune/query/PhortunePaymentMethodQuery.php', 'PhortunePaymentMethodViewController' => 'applications/phortune/controller/PhortunePaymentMethodViewController.php', 'PhortuneProduct' => 'applications/phortune/storage/PhortuneProduct.php', + 'PhortuneProductEditController' => 'applications/phortune/controller/PhortuneProductEditController.php', + 'PhortuneProductEditor' => 'applications/phortune/editor/PhortuneProductEditor.php', + 'PhortuneProductListController' => 'applications/phortune/controller/PhortuneProductListController.php', + 'PhortuneProductQuery' => 'applications/phortune/query/PhortuneProductQuery.php', + 'PhortuneProductTransaction' => 'applications/phortune/storage/PhortuneProductTransaction.php', + 'PhortuneProductTransactionQuery' => 'applications/phortune/query/PhortuneProductTransactionQuery.php', + 'PhortuneProductViewController' => 'applications/phortune/controller/PhortuneProductViewController.php', 'PhortunePurchase' => 'applications/phortune/storage/PhortunePurchase.php', 'PhortuneStripePaymentFormView' => 'applications/phortune/view/PhortuneStripePaymentFormView.php', + 'PhortuneUtil' => 'applications/phortune/util/PhortuneUtil.php', 'PhrictionActionConstants' => 'applications/phriction/constants/PhrictionActionConstants.php', 'PhrictionChangeType' => 'applications/phriction/constants/PhrictionChangeType.php', 'PhrictionConstants' => 'applications/phriction/constants/PhrictionConstants.php', @@ -3200,7 +3208,18 @@ phutil_register_library_map(array( 'PhortunePaymentMethodListController' => 'PhabricatorController', 'PhortunePaymentMethodQuery' => 'PhabricatorCursorPagedPolicyAwareQuery', 'PhortunePaymentMethodViewController' => 'PhabricatorController', - 'PhortuneProduct' => 'PhortuneDAO', + 'PhortuneProduct' => + array( + 0 => 'PhortuneDAO', + 1 => 'PhabricatorPolicyInterface', + ), + 'PhortuneProductEditController' => 'PhabricatorController', + 'PhortuneProductEditor' => 'PhabricatorApplicationTransactionEditor', + 'PhortuneProductListController' => 'PhabricatorController', + 'PhortuneProductQuery' => 'PhabricatorCursorPagedPolicyAwareQuery', + 'PhortuneProductTransaction' => 'PhabricatorApplicationTransaction', + 'PhortuneProductTransactionQuery' => 'PhabricatorApplicationTransactionQuery', + 'PhortuneProductViewController' => 'PhabricatorController', 'PhortunePurchase' => 'PhortuneDAO', 'PhortuneStripePaymentFormView' => 'AphrontView', 'PhrictionActionConstants' => 'PhrictionConstants', diff --git a/src/applications/phortune/application/PhabricatorApplicationPhortune.php b/src/applications/phortune/application/PhabricatorApplicationPhortune.php index 80c6d7b4a8..fda70d2b12 100644 --- a/src/applications/phortune/application/PhabricatorApplicationPhortune.php +++ b/src/applications/phortune/application/PhabricatorApplicationPhortune.php @@ -43,6 +43,11 @@ final class PhabricatorApplicationPhortune extends PhabricatorApplication { 'stripe/' => array( 'testpaymentform/' => 'PhortuneStripeTestPaymentFormController', ), + 'product/' => array( + '' => 'PhortuneProductListController', + 'view/(?P\d+)/' => 'PhortuneProductViewController', + 'edit/(?:(?P\d+)/)?' => 'PhortuneProductEditController', + ), ), ); } diff --git a/src/applications/phortune/controller/PhortuneAccountViewController.php b/src/applications/phortune/controller/PhortuneAccountViewController.php index b86820c5c2..41ece1b6d7 100644 --- a/src/applications/phortune/controller/PhortuneAccountViewController.php +++ b/src/applications/phortune/controller/PhortuneAccountViewController.php @@ -54,6 +54,7 @@ final class PhortuneAccountViewController extends PhortuneController { $properties->addProperty(pht('Balance'), $account->getBalanceInCents()); $payment_methods = $this->buildPaymentMethodsSection($account); + $purchase_history = $this->buildPurchaseHistorySection($account); $account_history = $this->buildAccountHistorySection($account); return $this->buildApplicationPage( @@ -63,6 +64,7 @@ final class PhortuneAccountViewController extends PhortuneController { $actions, $properties, $payment_methods, + $purchase_history, $account_history, ), array( @@ -136,6 +138,19 @@ final class PhortuneAccountViewController extends PhortuneController { ); } + private function buildPurchaseHistorySection(PhortuneAccount $account) { + $request = $this->getRequest(); + $user = $request->getUser(); + + $header = id(new PhabricatorHeaderView()) + ->setHeader(pht('Purchase History')); + + return array( + $header, + + ); + } + private function buildAccountHistorySection(PhortuneAccount $account) { $request = $this->getRequest(); $user = $request->getUser(); diff --git a/src/applications/phortune/controller/PhortuneProductEditController.php b/src/applications/phortune/controller/PhortuneProductEditController.php new file mode 100644 index 0000000000..18101fc255 --- /dev/null +++ b/src/applications/phortune/controller/PhortuneProductEditController.php @@ -0,0 +1,162 @@ +productID = idx($data, 'id'); + } + + public function processRequest() { + $request = $this->getRequest(); + $user = $request->getUser(); + + if ($this->productID) { + $product = id(new PhortuneProductQuery()) + ->setViewer($user) + ->withIDs(array($this->productID)) + ->executeOne(); + if (!$product) { + return new Aphront404Response(); + } + + $is_create = false; + $cancel_uri = $this->getApplicationURI( + 'product/view/'.$this->productID.'/'); + } else { + $product = new PhortuneProduct(); + $is_create = true; + $cancel_uri = $this->getApplicationURI('product/'); + } + + $v_name = $product->getProductName(); + $v_type = $product->getProductType(); + $v_price = (int)$product->getPriceInCents(); + $display_price = PhortuneUtil::formatCurrency($v_price); + + $e_name = true; + $e_type = null; + $e_price = true; + $errors = array(); + + if ($request->isFormPost()) { + $v_name = $request->getStr('name'); + if (!strlen($v_name)) { + $e_name = pht('Required'); + $errors[] = pht('Product must have a name.'); + } else { + $e_name = null; + } + + if ($is_create) { + $v_type = $request->getStr('type'); + $type_map = PhortuneProduct::getTypeMap(); + if (empty($type_map[$v_type])) { + $e_type = pht('Invalid'); + $errors[] = pht('Product type is invalid.'); + } else { + $e_type = null; + } + } + + $display_price = $request->getStr('price'); + try { + $v_price = PhortuneUtil::parseCurrency($display_price); + $e_price = null; + } catch (Exception $ex) { + $errors[] = pht('Price should be formatted as: $1.23'); + $e_price = pht('Invalid'); + } + + if (!$errors) { + $xactions = array(); + + $xactions[] = id(new PhortuneProductTransaction()) + ->setTransactionType(PhortuneProductTransaction::TYPE_NAME) + ->setNewValue($v_name); + + $xactions[] = id(new PhortuneProductTransaction()) + ->setTransactionType(PhortuneProductTransaction::TYPE_TYPE) + ->setNewValue($v_type); + + $xactions[] = id(new PhortuneProductTransaction()) + ->setTransactionType(PhortuneProductTransaction::TYPE_PRICE) + ->setNewValue($v_price); + + $editor = id(new PhortuneProductEditor()) + ->setActor($user) + ->setContinueOnNoEffect(true) + ->setContentSourceFromRequest($request); + + $editor->applyTransactions($product, $xactions); + + return id(new AphrontRedirectResponse())->setURI( + $this->getApplicationURI('product/view/'.$product->getID().'/')); + } + } + + if ($errors) { + $errors = id(new AphrontErrorView()) + ->setErrors($errors); + } + + $form = id(new AphrontFormView()) + ->setUser($user) + ->appendChild( + id(new AphrontFormTextControl()) + ->setLabel(pht('Name')) + ->setName('name') + ->setValue($v_name) + ->setError($e_name)) + ->appendChild( + id(new AphrontFormSelectControl()) + ->setLabel(pht('Type')) + ->setName('type') + ->setValue($v_type) + ->setError($e_type) + ->setOptions(PhortuneProduct::getTypeMap()) + ->setDisabled(!$is_create)) + ->appendChild( + id(new AphrontFormTextControl()) + ->setLabel(pht('Price')) + ->setName('price') + ->setValue($display_price) + ->setError($e_price)) + ->appendChild( + id(new AphrontFormSubmitControl()) + ->setValue( + $is_create + ? pht('Create Product') + : pht('Save Product')) + ->addCancelButton($cancel_uri)); + + $title = pht('Edit Product'); + $crumbs = $this->buildApplicationCrumbs(); + $crumbs->addCrumb( + id(new PhabricatorCrumbView()) + ->setName(pht('Products')) + ->setHref($this->getApplicationURI('product/'))); + $crumbs->addCrumb( + id(new PhabricatorCrumbView()) + ->setName($is_create ? pht('Create') : pht('Edit')) + ->setHref($request->getRequestURI())); + + $header = id(new PhabricatorHeaderView()) + ->setHeader(pht('Edit Product')); + + return $this->buildApplicationPage( + array( + $crumbs, + $header, + $errors, + $form, + ), + array( + 'title' => $title, + 'device' => true, + 'dust' => true, + )); + } + +} diff --git a/src/applications/phortune/controller/PhortuneProductListController.php b/src/applications/phortune/controller/PhortuneProductListController.php new file mode 100644 index 0000000000..a1cc6ba36b --- /dev/null +++ b/src/applications/phortune/controller/PhortuneProductListController.php @@ -0,0 +1,67 @@ +getRequest(); + $user = $request->getUser(); + + $pager = new AphrontCursorPagerView(); + $pager->readFromRequest($request); + + $query = id(new PhortuneProductQuery()) + ->setViewer($user); + + $products = $query->executeWithCursorPager($pager); + + $title = pht('Product List'); + + $crumbs = $this->buildApplicationCrumbs(); + $crumbs->addCrumb( + id(new PhabricatorCrumbView()) + ->setName('Products') + ->setHref($this->getApplicationURI('product/'))); + $crumbs->addAction( + id(new PhabricatorMenuItemView()) + ->setName(pht('Create Product')) + ->setHref($this->getApplicationURI('product/edit/')) + ->setIcon('create')); + + $header = id(new PhabricatorHeaderView()) + ->setHeader(pht('Product List')); + + $product_list = id(new PhabricatorObjectItemListView()) + ->setUser($user) + ->setNoDataString(pht('No products.')); + + foreach ($products as $product) { + $view_uri = $this->getApplicationURI( + 'product/view/'.$product->getID().'/'); + + $price = $product->getPriceInCents(); + + $item = id(new PhabricatorObjectItemView()) + ->setObjectName($product->getID()) + ->setHeader($product->getProductName()) + ->setHref($view_uri) + ->addAttribute(PhortuneUtil::formatCurrency($price)) + ->addAttribute($product->getTypeName()); + + $product_list->addItem($item); + } + + return $this->buildApplicationPage( + array( + $crumbs, + $header, + $product_list, + $pager, + ), + array( + 'title' => $title, + 'device' => true, + 'dust' => true, + )); + } + +} diff --git a/src/applications/phortune/controller/PhortuneProductViewController.php b/src/applications/phortune/controller/PhortuneProductViewController.php new file mode 100644 index 0000000000..907ea25802 --- /dev/null +++ b/src/applications/phortune/controller/PhortuneProductViewController.php @@ -0,0 +1,84 @@ +productID = $data['id']; + } + + public function processRequest() { + $request = $this->getRequest(); + $user = $request->getUser(); + + $product = id(new PhortuneProductQuery()) + ->setViewer($user) + ->withIDs(array($this->productID)) + ->executeOne(); + + if (!$product) { + return new Aphront404Response(); + } + + $title = pht('Product: %s', $product->getProductName()); + + $crumbs = $this->buildApplicationCrumbs(); + $crumbs->addCrumb( + id(new PhabricatorCrumbView()) + ->setName(pht('Products')) + ->setHref($this->getApplicationURI('product/'))); + $crumbs->addCrumb( + id(new PhabricatorCrumbView()) + ->setName(pht('#%d', $product->getID())) + ->setHref($request->getRequestURI())); + + $header = id(new PhabricatorHeaderView()) + ->setHeader($product->getProductName()); + + $edit_uri = $this->getApplicationURI('product/edit/'.$product->getID().'/'); + + $actions = id(new PhabricatorActionListView()) + ->setUser($user) + ->addAction( + id(new PhabricatorActionView()) + ->setName(pht('Edit Product')) + ->setHref($edit_uri) + ->setIcon('edit')); + + $properties = id(new PhabricatorPropertyListView()) + ->setUser($user) + ->addProperty(pht('Type'), $product->getTypeName()) + ->addProperty( + pht('Price'), + PhortuneUtil::formatCurrency($product->getPriceInCents())); + + $xactions = id(new PhortuneProductTransactionQuery()) + ->setViewer($user) + ->withObjectPHIDs(array($product->getPHID())) + ->execute(); + + $engine = id(new PhabricatorMarkupEngine()) + ->setViewer($user); + + $xaction_view = id(new PhabricatorApplicationTransactionView()) + ->setUser($user) + ->setTransactions($xactions) + ->setMarkupEngine($engine); + + return $this->buildApplicationPage( + array( + $crumbs, + $header, + $actions, + $properties, + $xaction_view, + ), + array( + 'title' => $title, + 'device' => true, + 'dust' => true, + )); + } + +} diff --git a/src/applications/phortune/editor/PhortuneProductEditor.php b/src/applications/phortune/editor/PhortuneProductEditor.php new file mode 100644 index 0000000000..a9b849cee4 --- /dev/null +++ b/src/applications/phortune/editor/PhortuneProductEditor.php @@ -0,0 +1,73 @@ +getTransactionType()) { + case PhortuneProductTransaction::TYPE_NAME: + return $object->getProductName(); + case PhortuneProductTransaction::TYPE_TYPE: + return $object->getProductType(); + case PhortuneProductTransaction::TYPE_PRICE: + return $object->getPriceInCents(); + } + return parent::getCustomTransactionOldValue($object, $xaction); + } + + protected function getCustomTransactionNewValue( + PhabricatorLiskDAO $object, + PhabricatorApplicationTransaction $xaction) { + switch ($xaction->getTransactionType()) { + case PhortuneProductTransaction::TYPE_NAME: + case PhortuneProductTransaction::TYPE_TYPE: + case PhortuneProductTransaction::TYPE_PRICE: + return $xaction->getNewValue(); + } + return parent::getCustomTransactionNewValue($object, $xaction); + } + + protected function applyCustomInternalTransaction( + PhabricatorLiskDAO $object, + PhabricatorApplicationTransaction $xaction) { + switch ($xaction->getTransactionType()) { + case PhortuneProductTransaction::TYPE_NAME: + $object->setProductName($xaction->getNewValue()); + return; + case PhortuneProductTransaction::TYPE_TYPE: + $object->setProductType($xaction->getNewValue()); + return; + case PhortuneProductTransaction::TYPE_PRICE: + $object->setPriceInCents($xaction->getNewValue()); + return; + } + return parent::applyCustomInternalTransaction($object, $xaction); + } + + protected function applyCustomExternalTransaction( + PhabricatorLiskDAO $object, + PhabricatorApplicationTransaction $xaction) { + switch ($xaction->getTransactionType()) { + case PhortuneProductTransaction::TYPE_NAME: + case PhortuneProductTransaction::TYPE_TYPE: + case PhortuneProductTransaction::TYPE_PRICE: + return; + } + return parent::applyCustomExternalTransaction($object, $xaction); + } + +} diff --git a/src/applications/phortune/query/PhortuneProductQuery.php b/src/applications/phortune/query/PhortuneProductQuery.php new file mode 100644 index 0000000000..e87518c019 --- /dev/null +++ b/src/applications/phortune/query/PhortuneProductQuery.php @@ -0,0 +1,56 @@ +ids = $ids; + return $this; + } + + public function withPHIDs(array $phids) { + $this->phids = $phids; + return $this; + } + + protected function loadPage() { + $table = new PhortuneProduct(); + $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) { + $where[] = qsprintf( + $conn, + 'id IN (%Ld)', + $this->ids); + } + + if ($this->phids) { + $where[] = qsprintf( + $conn, + 'phid IN (%Ls)', + $this->phids); + } + + $where[] = $this->buildPagingClause($conn); + + return $this->formatWhereClause($where); + } + +} diff --git a/src/applications/phortune/query/PhortuneProductTransactionQuery.php b/src/applications/phortune/query/PhortuneProductTransactionQuery.php new file mode 100644 index 0000000000..e64a43c7a8 --- /dev/null +++ b/src/applications/phortune/query/PhortuneProductTransactionQuery.php @@ -0,0 +1,10 @@ + pht('Product (Charged Once)'), + self::TYPE_BILL_PLAN => pht('Flat Rate Plan (Charged Monthly)'), + ); + } + public function getTypeName() { + return idx(self::getTypeMap(), $this->getProductType()); + } + + public function getPriceInCents() { + $price = parent::getPriceInCents(); + if ($price === null) { + return $price; + } else { + return (int)parent::getPriceInCents(); + } + } + + +/* -( PhabricatorPolicyInterface )----------------------------------------- */ + + + public function getCapabilities() { + return array( + PhabricatorPolicyCapability::CAN_VIEW, + ); + } + + public function getPolicy($capability) { + return PhabricatorPolicies::POLICY_USER; + } + + public function hasAutomaticCapability($capability, PhabricatorUser $viewer) { + return false; + } } diff --git a/src/applications/phortune/storage/PhortuneProductTransaction.php b/src/applications/phortune/storage/PhortuneProductTransaction.php new file mode 100644 index 0000000000..c06af5e81c --- /dev/null +++ b/src/applications/phortune/storage/PhortuneProductTransaction.php @@ -0,0 +1,80 @@ +getAuthorPHID(); + + $old = $this->getOldValue(); + $new = $this->getNewValue(); + + switch ($this->getTransactionType()) { + case self::TYPE_NAME: + if ($old === null) { + return pht( + '%s created this product.', + $this->renderHandleLink($author_phid)); + } else { + return pht( + '%s renamed this product from "%s" to "%s".', + $this->renderHandleLink($author_phid), + $old, + $new); + } + break; + case self::TYPE_PRICE: + if ($old === null) { + return pht( + '%s set product price to %s.', + $this->renderHandleLink($author_phid), + PhortuneUtil::formatCurrency($new)); + } else { + return pht( + '%s changed product price from %s to %s.', + $this->renderHandleLink($author_phid), + PhortuneUtil::formatCurrency($old), + PhortuneUtil::formatCurrency($new)); + } + break; + case self::TYPE_TYPE: + $map = PhortuneProduct::getTypeMap(); + if ($old === null) { + return pht( + '%s set product type to "%s".', + $this->renderHandleLink($author_phid), + $map[$new]); + } else { + return pht( + '%s changed product type from "%s" to "%s".', + $this->renderHandleLink($author_phid), + $map[$old], + $map[$new]); + } + break; + } + + return parent::getTitle(); + } + +} diff --git a/src/applications/phortune/storage/PhortunePurchase.php b/src/applications/phortune/storage/PhortunePurchase.php index d3e31d842e..e3f8590ccc 100644 --- a/src/applications/phortune/storage/PhortunePurchase.php +++ b/src/applications/phortune/storage/PhortunePurchase.php @@ -5,6 +5,7 @@ */ final class PhortunePurchase extends PhortuneDAO { + const STATUS_PENDING = 'purchase:pending'; const STATUS_PROCESSING = 'purchase:processing'; const STATUS_ACTIVE = 'purchase:active'; const STATUS_CANCELED = 'purchase:canceled'; @@ -14,9 +15,16 @@ final class PhortunePurchase extends PhortuneDAO { protected $productPHID; protected $accountPHID; protected $authorPHID; + protected $purchaseName; + protected $purchaseURI; protected $paymentMethodPHID; + protected $basePriceInCents; + protected $priceAdjustmentInCents; + protected $finalPriceInCents; protected $quantity; + protected $totalPriceInCents; protected $status; + protected $metadata; public function getConfiguration() { return array( diff --git a/src/applications/phortune/util/PhortuneUtil.php b/src/applications/phortune/util/PhortuneUtil.php new file mode 100644 index 0000000000..8ea076b5a1 --- /dev/null +++ b/src/applications/phortune/util/PhortuneUtil.php @@ -0,0 +1,34 @@ +setContentSource( + PhabricatorContentSource::newForSource( + PhabricatorContentSource::SOURCE_WEB, + array( + 'ip' => $request->getRemoteAddr(), + ))); + } + public function getContentSource() { return $this->contentSource; } diff --git a/src/infrastructure/storage/patch/PhabricatorBuiltinPatchList.php b/src/infrastructure/storage/patch/PhabricatorBuiltinPatchList.php index 04af976eae..48fc6ea24c 100644 --- a/src/infrastructure/storage/patch/PhabricatorBuiltinPatchList.php +++ b/src/infrastructure/storage/patch/PhabricatorBuiltinPatchList.php @@ -1210,6 +1210,10 @@ final class PhabricatorBuiltinPatchList extends PhabricatorSQLPatchList { 'type' => 'sql', 'name' => $this->getPatchPath('20130323.phortunepayment.sql'), ), + '20130324.phortuneproduct.sql' => array( + 'type' => 'sql', + 'name' => $this->getPatchPath('20130324.phortuneproduct.sql'), + ), ); }