mirror of
https://we.phorge.it/source/phorge.git
synced 2024-11-26 08:42:41 +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:
parent
4f3b5f0ea9
commit
abdaf90239
17 changed files with 749 additions and 45 deletions
35
resources/sql/patches/20130324.phortuneproduct.sql
Normal file
35
resources/sql/patches/20130324.phortuneproduct.sql
Normal 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;
|
|
@ -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',
|
||||
),
|
||||
));
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -43,6 +43,11 @@ final class PhabricatorApplicationPhortune extends PhabricatorApplication {
|
|||
'stripe/' => array(
|
||||
'testpaymentform/' => 'PhortuneStripeTestPaymentFormController',
|
||||
),
|
||||
'product/' => array(
|
||||
'' => 'PhortuneProductListController',
|
||||
'view/(?P<id>\d+)/' => 'PhortuneProductViewController',
|
||||
'edit/(?:(?P<id>\d+)/)?' => 'PhortuneProductEditController',
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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,
|
||||
));
|
||||
}
|
||||
|
||||
}
|
|
@ -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,
|
||||
));
|
||||
}
|
||||
|
||||
}
|
|
@ -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,
|
||||
));
|
||||
}
|
||||
|
||||
}
|
73
src/applications/phortune/editor/PhortuneProductEditor.php
Normal file
73
src/applications/phortune/editor/PhortuneProductEditor.php
Normal 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);
|
||||
}
|
||||
|
||||
}
|
56
src/applications/phortune/query/PhortuneProductQuery.php
Normal file
56
src/applications/phortune/query/PhortuneProductQuery.php
Normal 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);
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,10 @@
|
|||
<?php
|
||||
|
||||
final class PhortuneProductTransactionQuery
|
||||
extends PhabricatorApplicationTransactionQuery {
|
||||
|
||||
protected function getTemplateApplicationTransaction() {
|
||||
return new PhortuneProductTransaction();
|
||||
}
|
||||
|
||||
}
|
|
@ -4,12 +4,18 @@
|
|||
* A product is something users can purchase. It may be a one-time purchase,
|
||||
* 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_PLAN = 'phortune:plan';
|
||||
|
||||
const STATUS_ACTIVE = 'product:active';
|
||||
const STATUS_DISABLED = 'product:disabled';
|
||||
|
||||
protected $productName;
|
||||
protected $productType;
|
||||
protected $status = self::STATUS_ACTIVE;
|
||||
protected $priceInCents;
|
||||
protected $billingIntervalInMonths;
|
||||
protected $trialPeriodInDays;
|
||||
|
@ -29,6 +35,42 @@ final class PhortuneProduct extends PhortuneDAO {
|
|||
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;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
||||
}
|
|
@ -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(
|
||||
|
|
34
src/applications/phortune/util/PhortuneUtil.php
Normal file
34
src/applications/phortune/util/PhortuneUtil.php
Normal 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;
|
||||
}
|
||||
|
||||
}
|
|
@ -262,6 +262,15 @@ abstract class PhabricatorApplicationTransactionEditor
|
|||
return $this;
|
||||
}
|
||||
|
||||
public function setContentSourceFromRequest(AphrontRequest $request) {
|
||||
return $this->setContentSource(
|
||||
PhabricatorContentSource::newForSource(
|
||||
PhabricatorContentSource::SOURCE_WEB,
|
||||
array(
|
||||
'ip' => $request->getRemoteAddr(),
|
||||
)));
|
||||
}
|
||||
|
||||
public function getContentSource() {
|
||||
return $this->contentSource;
|
||||
}
|
||||
|
|
|
@ -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'),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
|
|
Loading…
Reference in a new issue