1
0
Fork 0
mirror of https://we.phorge.it/source/phorge.git synced 2024-11-22 23:02:42 +01:00

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
This commit is contained in:
epriestley 2013-03-28 09:13:07 -07:00
parent 4f3b5f0ea9
commit abdaf90239
17 changed files with 749 additions and 45 deletions

View file

@ -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;

View file

@ -2138,14 +2138,15 @@ celerity_register_resource_map(array(
), ),
'javelin-behavior-stripe-payment-form' => 'javelin-behavior-stripe-payment-form' =>
array( 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', 'type' => 'js',
'requires' => 'requires' =>
array( array(
0 => 'javelin-behavior', 0 => 'javelin-behavior',
1 => 'javelin-dom', 1 => 'javelin-dom',
2 => 'javelin-json', 2 => 'javelin-json',
3 => 'stripe-core', 3 => 'javelin-workflow',
4 => 'stripe-core',
), ),
'disk' => '/rsrc/js/application/phortune/behavior-stripe-payment-form.js', 'disk' => '/rsrc/js/application/phortune/behavior-stripe-payment-form.js',
), ),
@ -2883,7 +2884,7 @@ celerity_register_resource_map(array(
), ),
'phabricator-flag-css' => 'phabricator-flag-css' =>
array( array(
'uri' => '/res/2eee890a/rsrc/css/application/flag/flag.css', 'uri' => '/res/237234d7/rsrc/css/application/flag/flag.css',
'type' => 'css', 'type' => 'css',
'requires' => 'requires' =>
array( array(
@ -3704,7 +3705,7 @@ celerity_register_resource_map(array(
), array( ), array(
'packages' => 'packages' =>
array( array(
'20f728ee' => '4fc2a8f9' =>
array( array(
'name' => 'core.pkg.css', 'name' => 'core.pkg.css',
'symbols' => 'symbols' =>
@ -3746,7 +3747,7 @@ celerity_register_resource_map(array(
34 => 'phabricator-object-item-list-view-css', 34 => 'phabricator-object-item-list-view-css',
35 => 'global-drag-and-drop-css', 35 => 'global-drag-and-drop-css',
), ),
'uri' => '/res/pkg/20f728ee/core.pkg.css', 'uri' => '/res/pkg/4fc2a8f9/core.pkg.css',
'type' => 'css', 'type' => 'css',
), ),
'95ceba95' => '95ceba95' =>
@ -3937,16 +3938,16 @@ celerity_register_resource_map(array(
'reverse' => 'reverse' =>
array( array(
'aphront-attached-file-view-css' => '6b1fccc6', 'aphront-attached-file-view-css' => '6b1fccc6',
'aphront-dialog-view-css' => '20f728ee', 'aphront-dialog-view-css' => '4fc2a8f9',
'aphront-error-view-css' => '20f728ee', 'aphront-error-view-css' => '4fc2a8f9',
'aphront-form-view-css' => '20f728ee', 'aphront-form-view-css' => '4fc2a8f9',
'aphront-list-filter-view-css' => '20f728ee', 'aphront-list-filter-view-css' => '4fc2a8f9',
'aphront-pager-view-css' => '20f728ee', 'aphront-pager-view-css' => '4fc2a8f9',
'aphront-panel-view-css' => '20f728ee', 'aphront-panel-view-css' => '4fc2a8f9',
'aphront-table-view-css' => '20f728ee', 'aphront-table-view-css' => '4fc2a8f9',
'aphront-tokenizer-control-css' => '20f728ee', 'aphront-tokenizer-control-css' => '4fc2a8f9',
'aphront-tooltip-css' => '20f728ee', 'aphront-tooltip-css' => '4fc2a8f9',
'aphront-typeahead-control-css' => '20f728ee', 'aphront-typeahead-control-css' => '4fc2a8f9',
'differential-changeset-view-css' => '8aaacd1b', 'differential-changeset-view-css' => '8aaacd1b',
'differential-core-view-css' => '8aaacd1b', 'differential-core-view-css' => '8aaacd1b',
'differential-inline-comment-editor' => '322728f3', 'differential-inline-comment-editor' => '322728f3',
@ -3960,7 +3961,7 @@ celerity_register_resource_map(array(
'differential-table-of-contents-css' => '8aaacd1b', 'differential-table-of-contents-css' => '8aaacd1b',
'diffusion-commit-view-css' => 'c8ce2d88', 'diffusion-commit-view-css' => 'c8ce2d88',
'diffusion-icons-css' => 'c8ce2d88', 'diffusion-icons-css' => 'c8ce2d88',
'global-drag-and-drop-css' => '20f728ee', 'global-drag-and-drop-css' => '4fc2a8f9',
'inline-comment-summary-css' => '8aaacd1b', 'inline-comment-summary-css' => '8aaacd1b',
'javelin-aphlict' => '95ceba95', 'javelin-aphlict' => '95ceba95',
'javelin-behavior' => 'fe22443b', 'javelin-behavior' => 'fe22443b',
@ -4032,48 +4033,48 @@ celerity_register_resource_map(array(
'javelin-util' => 'fe22443b', 'javelin-util' => 'fe22443b',
'javelin-vector' => 'fe22443b', 'javelin-vector' => 'fe22443b',
'javelin-workflow' => 'fe22443b', 'javelin-workflow' => 'fe22443b',
'lightbox-attachment-css' => '20f728ee', 'lightbox-attachment-css' => '4fc2a8f9',
'maniphest-task-summary-css' => '6b1fccc6', 'maniphest-task-summary-css' => '6b1fccc6',
'maniphest-transaction-detail-css' => '6b1fccc6', 'maniphest-transaction-detail-css' => '6b1fccc6',
'phabricator-busy' => '95ceba95', 'phabricator-busy' => '95ceba95',
'phabricator-content-source-view-css' => '8aaacd1b', 'phabricator-content-source-view-css' => '8aaacd1b',
'phabricator-core-buttons-css' => '20f728ee', 'phabricator-core-buttons-css' => '4fc2a8f9',
'phabricator-core-css' => '20f728ee', 'phabricator-core-css' => '4fc2a8f9',
'phabricator-crumbs-view-css' => '20f728ee', 'phabricator-crumbs-view-css' => '4fc2a8f9',
'phabricator-directory-css' => '20f728ee', 'phabricator-directory-css' => '4fc2a8f9',
'phabricator-drag-and-drop-file-upload' => '322728f3', 'phabricator-drag-and-drop-file-upload' => '322728f3',
'phabricator-dropdown-menu' => '95ceba95', 'phabricator-dropdown-menu' => '95ceba95',
'phabricator-file-upload' => '95ceba95', 'phabricator-file-upload' => '95ceba95',
'phabricator-filetree-view-css' => '20f728ee', 'phabricator-filetree-view-css' => '4fc2a8f9',
'phabricator-flag-css' => '20f728ee', 'phabricator-flag-css' => '4fc2a8f9',
'phabricator-form-view-css' => '20f728ee', 'phabricator-form-view-css' => '4fc2a8f9',
'phabricator-header-view-css' => '20f728ee', 'phabricator-header-view-css' => '4fc2a8f9',
'phabricator-jump-nav' => '20f728ee', 'phabricator-jump-nav' => '4fc2a8f9',
'phabricator-keyboard-shortcut' => '95ceba95', 'phabricator-keyboard-shortcut' => '95ceba95',
'phabricator-keyboard-shortcut-manager' => '95ceba95', 'phabricator-keyboard-shortcut-manager' => '95ceba95',
'phabricator-main-menu-view' => '20f728ee', 'phabricator-main-menu-view' => '4fc2a8f9',
'phabricator-menu-item' => '95ceba95', 'phabricator-menu-item' => '95ceba95',
'phabricator-nav-view-css' => '20f728ee', 'phabricator-nav-view-css' => '4fc2a8f9',
'phabricator-notification' => '95ceba95', 'phabricator-notification' => '95ceba95',
'phabricator-notification-css' => '20f728ee', 'phabricator-notification-css' => '4fc2a8f9',
'phabricator-notification-menu-css' => '20f728ee', 'phabricator-notification-menu-css' => '4fc2a8f9',
'phabricator-object-item-list-view-css' => '20f728ee', 'phabricator-object-item-list-view-css' => '4fc2a8f9',
'phabricator-object-selector-css' => '8aaacd1b', 'phabricator-object-selector-css' => '8aaacd1b',
'phabricator-paste-file-upload' => '95ceba95', 'phabricator-paste-file-upload' => '95ceba95',
'phabricator-prefab' => '95ceba95', 'phabricator-prefab' => '95ceba95',
'phabricator-project-tag-css' => '6b1fccc6', 'phabricator-project-tag-css' => '6b1fccc6',
'phabricator-remarkup-css' => '20f728ee', 'phabricator-remarkup-css' => '4fc2a8f9',
'phabricator-shaped-request' => '322728f3', 'phabricator-shaped-request' => '322728f3',
'phabricator-side-menu-view-css' => '20f728ee', 'phabricator-side-menu-view-css' => '4fc2a8f9',
'phabricator-standard-page-view' => '20f728ee', 'phabricator-standard-page-view' => '4fc2a8f9',
'phabricator-textareautils' => '95ceba95', 'phabricator-textareautils' => '95ceba95',
'phabricator-tooltip' => '95ceba95', 'phabricator-tooltip' => '95ceba95',
'phabricator-transaction-view-css' => '20f728ee', 'phabricator-transaction-view-css' => '4fc2a8f9',
'phabricator-zindex-css' => '20f728ee', 'phabricator-zindex-css' => '4fc2a8f9',
'sprite-apps-large-css' => '20f728ee', 'sprite-apps-large-css' => '4fc2a8f9',
'sprite-gradient-css' => '20f728ee', 'sprite-gradient-css' => '4fc2a8f9',
'sprite-icon-css' => '20f728ee', 'sprite-icon-css' => '4fc2a8f9',
'sprite-menu-css' => '20f728ee', 'sprite-menu-css' => '4fc2a8f9',
'syntax-highlighting-css' => '20f728ee', 'syntax-highlighting-css' => '4fc2a8f9',
), ),
)); ));

View file

@ -1546,8 +1546,16 @@ phutil_register_library_map(array(
'PhortunePaymentMethodQuery' => 'applications/phortune/query/PhortunePaymentMethodQuery.php', 'PhortunePaymentMethodQuery' => 'applications/phortune/query/PhortunePaymentMethodQuery.php',
'PhortunePaymentMethodViewController' => 'applications/phortune/controller/PhortunePaymentMethodViewController.php', 'PhortunePaymentMethodViewController' => 'applications/phortune/controller/PhortunePaymentMethodViewController.php',
'PhortuneProduct' => 'applications/phortune/storage/PhortuneProduct.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', 'PhortunePurchase' => 'applications/phortune/storage/PhortunePurchase.php',
'PhortuneStripePaymentFormView' => 'applications/phortune/view/PhortuneStripePaymentFormView.php', 'PhortuneStripePaymentFormView' => 'applications/phortune/view/PhortuneStripePaymentFormView.php',
'PhortuneUtil' => 'applications/phortune/util/PhortuneUtil.php',
'PhrictionActionConstants' => 'applications/phriction/constants/PhrictionActionConstants.php', 'PhrictionActionConstants' => 'applications/phriction/constants/PhrictionActionConstants.php',
'PhrictionChangeType' => 'applications/phriction/constants/PhrictionChangeType.php', 'PhrictionChangeType' => 'applications/phriction/constants/PhrictionChangeType.php',
'PhrictionConstants' => 'applications/phriction/constants/PhrictionConstants.php', 'PhrictionConstants' => 'applications/phriction/constants/PhrictionConstants.php',
@ -3200,7 +3208,18 @@ phutil_register_library_map(array(
'PhortunePaymentMethodListController' => 'PhabricatorController', 'PhortunePaymentMethodListController' => 'PhabricatorController',
'PhortunePaymentMethodQuery' => 'PhabricatorCursorPagedPolicyAwareQuery', 'PhortunePaymentMethodQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
'PhortunePaymentMethodViewController' => 'PhabricatorController', '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', 'PhortunePurchase' => 'PhortuneDAO',
'PhortuneStripePaymentFormView' => 'AphrontView', 'PhortuneStripePaymentFormView' => 'AphrontView',
'PhrictionActionConstants' => 'PhrictionConstants', 'PhrictionActionConstants' => 'PhrictionConstants',

View file

@ -43,6 +43,11 @@ final class PhabricatorApplicationPhortune extends PhabricatorApplication {
'stripe/' => array( 'stripe/' => array(
'testpaymentform/' => 'PhortuneStripeTestPaymentFormController', 'testpaymentform/' => 'PhortuneStripeTestPaymentFormController',
), ),
'product/' => array(
'' => 'PhortuneProductListController',
'view/(?P<id>\d+)/' => 'PhortuneProductViewController',
'edit/(?:(?P<id>\d+)/)?' => 'PhortuneProductEditController',
),
), ),
); );
} }

View file

@ -54,6 +54,7 @@ final class PhortuneAccountViewController extends PhortuneController {
$properties->addProperty(pht('Balance'), $account->getBalanceInCents()); $properties->addProperty(pht('Balance'), $account->getBalanceInCents());
$payment_methods = $this->buildPaymentMethodsSection($account); $payment_methods = $this->buildPaymentMethodsSection($account);
$purchase_history = $this->buildPurchaseHistorySection($account);
$account_history = $this->buildAccountHistorySection($account); $account_history = $this->buildAccountHistorySection($account);
return $this->buildApplicationPage( return $this->buildApplicationPage(
@ -63,6 +64,7 @@ final class PhortuneAccountViewController extends PhortuneController {
$actions, $actions,
$properties, $properties,
$payment_methods, $payment_methods,
$purchase_history,
$account_history, $account_history,
), ),
array( 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) { private function buildAccountHistorySection(PhortuneAccount $account) {
$request = $this->getRequest(); $request = $this->getRequest();
$user = $request->getUser(); $user = $request->getUser();

View file

@ -0,0 +1,162 @@
<?php
final class PhortuneProductEditController extends PhabricatorController {
private $productID;
public function willProcessRequest(array $data) {
$this->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,
));
}
}

View file

@ -0,0 +1,67 @@
<?php
final class PhortuneProductListController extends PhabricatorController {
public function processRequest() {
$request = $this->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,
));
}
}

View file

@ -0,0 +1,84 @@
<?php
final class PhortuneProductViewController extends PhabricatorController {
private $productID;
public function willProcessRequest(array $data) {
$this->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,
));
}
}

View file

@ -0,0 +1,73 @@
<?php
final class PhortuneProductEditor
extends PhabricatorApplicationTransactionEditor {
public function getTransactionTypes() {
$types = parent::getTransactionTypes();
$types[] = PhortuneProductTransaction::TYPE_NAME;
$types[] = PhortuneProductTransaction::TYPE_TYPE;
$types[] = PhortuneProductTransaction::TYPE_PRICE;
return $types;
}
protected function getCustomTransactionOldValue(
PhabricatorLiskDAO $object,
PhabricatorApplicationTransaction $xaction) {
switch ($xaction->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);
}
}

View file

@ -0,0 +1,56 @@
<?php
final class PhortuneProductQuery
extends PhabricatorCursorPagedPolicyAwareQuery {
private $ids;
private $phids;
public function withIDs(array $ids) {
$this->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);
}
}

View file

@ -0,0 +1,10 @@
<?php
final class PhortuneProductTransactionQuery
extends PhabricatorApplicationTransactionQuery {
protected function getTemplateApplicationTransaction() {
return new PhortuneProductTransaction();
}
}

View file

@ -4,12 +4,18 @@
* A product is something users can purchase. It may be a one-time purchase, * A product is something users can purchase. It may be a one-time purchase,
* or a plan which is billed monthly. * or a plan which is billed monthly.
*/ */
final class PhortuneProduct extends PhortuneDAO { final class PhortuneProduct extends PhortuneDAO
implements PhabricatorPolicyInterface {
const TYPE_BILL_ONCE = 'phortune:thing'; const TYPE_BILL_ONCE = 'phortune:thing';
const TYPE_BILL_PLAN = 'phortune:plan'; const TYPE_BILL_PLAN = 'phortune:plan';
const STATUS_ACTIVE = 'product:active';
const STATUS_DISABLED = 'product:disabled';
protected $productName; protected $productName;
protected $productType;
protected $status = self::STATUS_ACTIVE;
protected $priceInCents; protected $priceInCents;
protected $billingIntervalInMonths; protected $billingIntervalInMonths;
protected $trialPeriodInDays; protected $trialPeriodInDays;
@ -29,6 +35,42 @@ final class PhortuneProduct extends PhortuneDAO {
PhabricatorPHIDConstants::PHID_TYPE_PDCT); PhabricatorPHIDConstants::PHID_TYPE_PDCT);
} }
public static function getTypeMap() {
return array(
self::TYPE_BILL_ONCE => 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;
}
} }

View file

@ -0,0 +1,80 @@
<?php
final class PhortuneProductTransaction
extends PhabricatorApplicationTransaction {
const TYPE_NAME = 'product:name';
const TYPE_TYPE = 'product:type';
const TYPE_PRICE = 'product:price';
public function getApplicationName() {
return 'phortune';
}
public function getApplicationTransactionType() {
return PhabricatorPHIDConstants::PHID_TYPE_PDCT;
}
public function getApplicationTransactionCommentObject() {
return null;
}
public function getApplicationObjectTypeName() {
return pht('product');
}
public function getTitle() {
$author_phid = $this->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();
}
}

View file

@ -5,6 +5,7 @@
*/ */
final class PhortunePurchase extends PhortuneDAO { final class PhortunePurchase extends PhortuneDAO {
const STATUS_PENDING = 'purchase:pending';
const STATUS_PROCESSING = 'purchase:processing'; const STATUS_PROCESSING = 'purchase:processing';
const STATUS_ACTIVE = 'purchase:active'; const STATUS_ACTIVE = 'purchase:active';
const STATUS_CANCELED = 'purchase:canceled'; const STATUS_CANCELED = 'purchase:canceled';
@ -14,9 +15,16 @@ final class PhortunePurchase extends PhortuneDAO {
protected $productPHID; protected $productPHID;
protected $accountPHID; protected $accountPHID;
protected $authorPHID; protected $authorPHID;
protected $purchaseName;
protected $purchaseURI;
protected $paymentMethodPHID; protected $paymentMethodPHID;
protected $basePriceInCents;
protected $priceAdjustmentInCents;
protected $finalPriceInCents;
protected $quantity; protected $quantity;
protected $totalPriceInCents;
protected $status; protected $status;
protected $metadata;
public function getConfiguration() { public function getConfiguration() {
return array( return array(

View file

@ -0,0 +1,34 @@
<?php
final class PhortuneUtil {
public static function parseCurrency($string) {
$string = trim($string);
if (!preg_match('/^[-]?[$]?\d+([.]\d{0,2})?$/', $string)) {
throw new Exception("Invalid currency format ('{$string}').");
}
$value = str_replace('$', '', $string);
$value = (float)$value;
$value = (int)round(100 * $value);
return $value;
}
public static function formatCurrency($price_in_cents) {
if (!preg_match('/^[-]?\d+$/', $price_in_cents)) {
throw new Exception("Invalid price in cents ('{$price_in_cents}').");
}
$negative = ($price_in_cents < 0);
$price_in_cents = abs($price_in_cents);
$display_value = sprintf('$%.02f', $price_in_cents / 100);
if ($negative) {
$display_value = '-'.$display_value;
}
return $display_value;
}
}

View file

@ -262,6 +262,15 @@ abstract class PhabricatorApplicationTransactionEditor
return $this; return $this;
} }
public function setContentSourceFromRequest(AphrontRequest $request) {
return $this->setContentSource(
PhabricatorContentSource::newForSource(
PhabricatorContentSource::SOURCE_WEB,
array(
'ip' => $request->getRemoteAddr(),
)));
}
public function getContentSource() { public function getContentSource() {
return $this->contentSource; return $this->contentSource;
} }

View file

@ -1210,6 +1210,10 @@ final class PhabricatorBuiltinPatchList extends PhabricatorSQLPatchList {
'type' => 'sql', 'type' => 'sql',
'name' => $this->getPatchPath('20130323.phortunepayment.sql'), 'name' => $this->getPatchPath('20130323.phortunepayment.sql'),
), ),
'20130324.phortuneproduct.sql' => array(
'type' => 'sql',
'name' => $this->getPatchPath('20130324.phortuneproduct.sql'),
),
); );
} }