@recaptime-dev's working patches + fork for Phorge, a community fork of Phabricator. (Upstream dev and stable branches are at upstream/main and upstream/stable respectively.) hq.recaptime.dev/wiki/Phorge
phorge phabricator
1
fork

Configure Feed

Select the types of activity you want to include in your feed.

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

+749 -45
+35
resources/sql/patches/20130324.phortuneproduct.sql
··· 1 + CREATE TABLE {$NAMESPACE}_phortune.phortune_product ( 2 + id INT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY, 3 + phid VARCHAR(64) NOT NULL COLLATE utf8_bin, 4 + productName VARCHAR(255) NOT NULL, 5 + productType VARCHAR(64) NOT NULL COLLATE utf8_bin, 6 + status VARCHAR(64) NOT NULL COLLATE utf8_bin, 7 + priceInCents BIGINT NOT NULL, 8 + billingIntervalInMonths INT UNSIGNED, 9 + trialPeriodInDays INT UNSIGNED, 10 + metadata LONGTEXT NOT NULL COLLATE utf8_bin, 11 + dateCreated INT UNSIGNED NOT NULL, 12 + dateModified INT UNSIGNED NOT NULL, 13 + UNIQUE KEY `key_phid` (phid), 14 + KEY `key_status` (status) 15 + ) ENGINE=InnoDB DEFAULT CHARSET=utf8; 16 + 17 + CREATE TABLE {$NAMESPACE}_phortune.phortune_producttransaction ( 18 + id INT UNSIGNED NOT NULL PRIMARY KEY AUTO_INCREMENT, 19 + phid VARCHAR(64) NOT NULL COLLATE utf8_bin, 20 + authorPHID VARCHAR(64) NOT NULL COLLATE utf8_bin, 21 + objectPHID VARCHAR(64) NOT NULL COLLATE utf8_bin, 22 + viewPolicy VARCHAR(64) NOT NULL COLLATE utf8_bin, 23 + editPolicy VARCHAR(64) NOT NULL COLLATE utf8_bin, 24 + commentPHID VARCHAR(64) COLLATE utf8_bin, 25 + commentVersion INT UNSIGNED NOT NULL, 26 + transactionType VARCHAR(32) NOT NULL COLLATE utf8_bin, 27 + oldValue LONGTEXT NOT NULL COLLATE utf8_bin, 28 + newValue LONGTEXT NOT NULL COLLATE utf8_bin, 29 + contentSource LONGTEXT NOT NULL COLLATE utf8_bin, 30 + metadata LONGTEXT NOT NULL COLLATE utf8_bin, 31 + dateCreated INT UNSIGNED NOT NULL, 32 + dateModified INT UNSIGNED NOT NULL, 33 + UNIQUE KEY `key_phid` (phid), 34 + KEY `key_object` (objectPHID) 35 + ) ENGINE=InnoDB DEFAULT CHARSET=utf8;
+42 -41
src/__celerity_resource_map__.php
··· 2138 2138 ), 2139 2139 'javelin-behavior-stripe-payment-form' => 2140 2140 array( 2141 - 'uri' => '/res/87c7b043/rsrc/js/application/phortune/behavior-stripe-payment-form.js', 2141 + 'uri' => '/res/30bcbbb1/rsrc/js/application/phortune/behavior-stripe-payment-form.js', 2142 2142 'type' => 'js', 2143 2143 'requires' => 2144 2144 array( 2145 2145 0 => 'javelin-behavior', 2146 2146 1 => 'javelin-dom', 2147 2147 2 => 'javelin-json', 2148 - 3 => 'stripe-core', 2148 + 3 => 'javelin-workflow', 2149 + 4 => 'stripe-core', 2149 2150 ), 2150 2151 'disk' => '/rsrc/js/application/phortune/behavior-stripe-payment-form.js', 2151 2152 ), ··· 2883 2884 ), 2884 2885 'phabricator-flag-css' => 2885 2886 array( 2886 - 'uri' => '/res/2eee890a/rsrc/css/application/flag/flag.css', 2887 + 'uri' => '/res/237234d7/rsrc/css/application/flag/flag.css', 2887 2888 'type' => 'css', 2888 2889 'requires' => 2889 2890 array( ··· 3704 3705 ), array( 3705 3706 'packages' => 3706 3707 array( 3707 - '20f728ee' => 3708 + '4fc2a8f9' => 3708 3709 array( 3709 3710 'name' => 'core.pkg.css', 3710 3711 'symbols' => ··· 3746 3747 34 => 'phabricator-object-item-list-view-css', 3747 3748 35 => 'global-drag-and-drop-css', 3748 3749 ), 3749 - 'uri' => '/res/pkg/20f728ee/core.pkg.css', 3750 + 'uri' => '/res/pkg/4fc2a8f9/core.pkg.css', 3750 3751 'type' => 'css', 3751 3752 ), 3752 3753 '95ceba95' => ··· 3937 3938 'reverse' => 3938 3939 array( 3939 3940 'aphront-attached-file-view-css' => '6b1fccc6', 3940 - 'aphront-dialog-view-css' => '20f728ee', 3941 - 'aphront-error-view-css' => '20f728ee', 3942 - 'aphront-form-view-css' => '20f728ee', 3943 - 'aphront-list-filter-view-css' => '20f728ee', 3944 - 'aphront-pager-view-css' => '20f728ee', 3945 - 'aphront-panel-view-css' => '20f728ee', 3946 - 'aphront-table-view-css' => '20f728ee', 3947 - 'aphront-tokenizer-control-css' => '20f728ee', 3948 - 'aphront-tooltip-css' => '20f728ee', 3949 - 'aphront-typeahead-control-css' => '20f728ee', 3941 + 'aphront-dialog-view-css' => '4fc2a8f9', 3942 + 'aphront-error-view-css' => '4fc2a8f9', 3943 + 'aphront-form-view-css' => '4fc2a8f9', 3944 + 'aphront-list-filter-view-css' => '4fc2a8f9', 3945 + 'aphront-pager-view-css' => '4fc2a8f9', 3946 + 'aphront-panel-view-css' => '4fc2a8f9', 3947 + 'aphront-table-view-css' => '4fc2a8f9', 3948 + 'aphront-tokenizer-control-css' => '4fc2a8f9', 3949 + 'aphront-tooltip-css' => '4fc2a8f9', 3950 + 'aphront-typeahead-control-css' => '4fc2a8f9', 3950 3951 'differential-changeset-view-css' => '8aaacd1b', 3951 3952 'differential-core-view-css' => '8aaacd1b', 3952 3953 'differential-inline-comment-editor' => '322728f3', ··· 3960 3961 'differential-table-of-contents-css' => '8aaacd1b', 3961 3962 'diffusion-commit-view-css' => 'c8ce2d88', 3962 3963 'diffusion-icons-css' => 'c8ce2d88', 3963 - 'global-drag-and-drop-css' => '20f728ee', 3964 + 'global-drag-and-drop-css' => '4fc2a8f9', 3964 3965 'inline-comment-summary-css' => '8aaacd1b', 3965 3966 'javelin-aphlict' => '95ceba95', 3966 3967 'javelin-behavior' => 'fe22443b', ··· 4032 4033 'javelin-util' => 'fe22443b', 4033 4034 'javelin-vector' => 'fe22443b', 4034 4035 'javelin-workflow' => 'fe22443b', 4035 - 'lightbox-attachment-css' => '20f728ee', 4036 + 'lightbox-attachment-css' => '4fc2a8f9', 4036 4037 'maniphest-task-summary-css' => '6b1fccc6', 4037 4038 'maniphest-transaction-detail-css' => '6b1fccc6', 4038 4039 'phabricator-busy' => '95ceba95', 4039 4040 'phabricator-content-source-view-css' => '8aaacd1b', 4040 - 'phabricator-core-buttons-css' => '20f728ee', 4041 - 'phabricator-core-css' => '20f728ee', 4042 - 'phabricator-crumbs-view-css' => '20f728ee', 4043 - 'phabricator-directory-css' => '20f728ee', 4041 + 'phabricator-core-buttons-css' => '4fc2a8f9', 4042 + 'phabricator-core-css' => '4fc2a8f9', 4043 + 'phabricator-crumbs-view-css' => '4fc2a8f9', 4044 + 'phabricator-directory-css' => '4fc2a8f9', 4044 4045 'phabricator-drag-and-drop-file-upload' => '322728f3', 4045 4046 'phabricator-dropdown-menu' => '95ceba95', 4046 4047 'phabricator-file-upload' => '95ceba95', 4047 - 'phabricator-filetree-view-css' => '20f728ee', 4048 - 'phabricator-flag-css' => '20f728ee', 4049 - 'phabricator-form-view-css' => '20f728ee', 4050 - 'phabricator-header-view-css' => '20f728ee', 4051 - 'phabricator-jump-nav' => '20f728ee', 4048 + 'phabricator-filetree-view-css' => '4fc2a8f9', 4049 + 'phabricator-flag-css' => '4fc2a8f9', 4050 + 'phabricator-form-view-css' => '4fc2a8f9', 4051 + 'phabricator-header-view-css' => '4fc2a8f9', 4052 + 'phabricator-jump-nav' => '4fc2a8f9', 4052 4053 'phabricator-keyboard-shortcut' => '95ceba95', 4053 4054 'phabricator-keyboard-shortcut-manager' => '95ceba95', 4054 - 'phabricator-main-menu-view' => '20f728ee', 4055 + 'phabricator-main-menu-view' => '4fc2a8f9', 4055 4056 'phabricator-menu-item' => '95ceba95', 4056 - 'phabricator-nav-view-css' => '20f728ee', 4057 + 'phabricator-nav-view-css' => '4fc2a8f9', 4057 4058 'phabricator-notification' => '95ceba95', 4058 - 'phabricator-notification-css' => '20f728ee', 4059 - 'phabricator-notification-menu-css' => '20f728ee', 4060 - 'phabricator-object-item-list-view-css' => '20f728ee', 4059 + 'phabricator-notification-css' => '4fc2a8f9', 4060 + 'phabricator-notification-menu-css' => '4fc2a8f9', 4061 + 'phabricator-object-item-list-view-css' => '4fc2a8f9', 4061 4062 'phabricator-object-selector-css' => '8aaacd1b', 4062 4063 'phabricator-paste-file-upload' => '95ceba95', 4063 4064 'phabricator-prefab' => '95ceba95', 4064 4065 'phabricator-project-tag-css' => '6b1fccc6', 4065 - 'phabricator-remarkup-css' => '20f728ee', 4066 + 'phabricator-remarkup-css' => '4fc2a8f9', 4066 4067 'phabricator-shaped-request' => '322728f3', 4067 - 'phabricator-side-menu-view-css' => '20f728ee', 4068 - 'phabricator-standard-page-view' => '20f728ee', 4068 + 'phabricator-side-menu-view-css' => '4fc2a8f9', 4069 + 'phabricator-standard-page-view' => '4fc2a8f9', 4069 4070 'phabricator-textareautils' => '95ceba95', 4070 4071 'phabricator-tooltip' => '95ceba95', 4071 - 'phabricator-transaction-view-css' => '20f728ee', 4072 - 'phabricator-zindex-css' => '20f728ee', 4073 - 'sprite-apps-large-css' => '20f728ee', 4074 - 'sprite-gradient-css' => '20f728ee', 4075 - 'sprite-icon-css' => '20f728ee', 4076 - 'sprite-menu-css' => '20f728ee', 4077 - 'syntax-highlighting-css' => '20f728ee', 4072 + 'phabricator-transaction-view-css' => '4fc2a8f9', 4073 + 'phabricator-zindex-css' => '4fc2a8f9', 4074 + 'sprite-apps-large-css' => '4fc2a8f9', 4075 + 'sprite-gradient-css' => '4fc2a8f9', 4076 + 'sprite-icon-css' => '4fc2a8f9', 4077 + 'sprite-menu-css' => '4fc2a8f9', 4078 + 'syntax-highlighting-css' => '4fc2a8f9', 4078 4079 ), 4079 4080 ));
+20 -1
src/__phutil_library_map__.php
··· 1546 1546 'PhortunePaymentMethodQuery' => 'applications/phortune/query/PhortunePaymentMethodQuery.php', 1547 1547 'PhortunePaymentMethodViewController' => 'applications/phortune/controller/PhortunePaymentMethodViewController.php', 1548 1548 'PhortuneProduct' => 'applications/phortune/storage/PhortuneProduct.php', 1549 + 'PhortuneProductEditController' => 'applications/phortune/controller/PhortuneProductEditController.php', 1550 + 'PhortuneProductEditor' => 'applications/phortune/editor/PhortuneProductEditor.php', 1551 + 'PhortuneProductListController' => 'applications/phortune/controller/PhortuneProductListController.php', 1552 + 'PhortuneProductQuery' => 'applications/phortune/query/PhortuneProductQuery.php', 1553 + 'PhortuneProductTransaction' => 'applications/phortune/storage/PhortuneProductTransaction.php', 1554 + 'PhortuneProductTransactionQuery' => 'applications/phortune/query/PhortuneProductTransactionQuery.php', 1555 + 'PhortuneProductViewController' => 'applications/phortune/controller/PhortuneProductViewController.php', 1549 1556 'PhortunePurchase' => 'applications/phortune/storage/PhortunePurchase.php', 1550 1557 'PhortuneStripePaymentFormView' => 'applications/phortune/view/PhortuneStripePaymentFormView.php', 1558 + 'PhortuneUtil' => 'applications/phortune/util/PhortuneUtil.php', 1551 1559 'PhrictionActionConstants' => 'applications/phriction/constants/PhrictionActionConstants.php', 1552 1560 'PhrictionChangeType' => 'applications/phriction/constants/PhrictionChangeType.php', 1553 1561 'PhrictionConstants' => 'applications/phriction/constants/PhrictionConstants.php', ··· 3200 3208 'PhortunePaymentMethodListController' => 'PhabricatorController', 3201 3209 'PhortunePaymentMethodQuery' => 'PhabricatorCursorPagedPolicyAwareQuery', 3202 3210 'PhortunePaymentMethodViewController' => 'PhabricatorController', 3203 - 'PhortuneProduct' => 'PhortuneDAO', 3211 + 'PhortuneProduct' => 3212 + array( 3213 + 0 => 'PhortuneDAO', 3214 + 1 => 'PhabricatorPolicyInterface', 3215 + ), 3216 + 'PhortuneProductEditController' => 'PhabricatorController', 3217 + 'PhortuneProductEditor' => 'PhabricatorApplicationTransactionEditor', 3218 + 'PhortuneProductListController' => 'PhabricatorController', 3219 + 'PhortuneProductQuery' => 'PhabricatorCursorPagedPolicyAwareQuery', 3220 + 'PhortuneProductTransaction' => 'PhabricatorApplicationTransaction', 3221 + 'PhortuneProductTransactionQuery' => 'PhabricatorApplicationTransactionQuery', 3222 + 'PhortuneProductViewController' => 'PhabricatorController', 3204 3223 'PhortunePurchase' => 'PhortuneDAO', 3205 3224 'PhortuneStripePaymentFormView' => 'AphrontView', 3206 3225 'PhrictionActionConstants' => 'PhrictionConstants',
+5
src/applications/phortune/application/PhabricatorApplicationPhortune.php
··· 43 43 'stripe/' => array( 44 44 'testpaymentform/' => 'PhortuneStripeTestPaymentFormController', 45 45 ), 46 + 'product/' => array( 47 + '' => 'PhortuneProductListController', 48 + 'view/(?P<id>\d+)/' => 'PhortuneProductViewController', 49 + 'edit/(?:(?P<id>\d+)/)?' => 'PhortuneProductEditController', 50 + ), 46 51 ), 47 52 ); 48 53 }
+15
src/applications/phortune/controller/PhortuneAccountViewController.php
··· 54 54 $properties->addProperty(pht('Balance'), $account->getBalanceInCents()); 55 55 56 56 $payment_methods = $this->buildPaymentMethodsSection($account); 57 + $purchase_history = $this->buildPurchaseHistorySection($account); 57 58 $account_history = $this->buildAccountHistorySection($account); 58 59 59 60 return $this->buildApplicationPage( ··· 63 64 $actions, 64 65 $properties, 65 66 $payment_methods, 67 + $purchase_history, 66 68 $account_history, 67 69 ), 68 70 array( ··· 133 135 $header, 134 136 $actions, 135 137 $list, 138 + ); 139 + } 140 + 141 + private function buildPurchaseHistorySection(PhortuneAccount $account) { 142 + $request = $this->getRequest(); 143 + $user = $request->getUser(); 144 + 145 + $header = id(new PhabricatorHeaderView()) 146 + ->setHeader(pht('Purchase History')); 147 + 148 + return array( 149 + $header, 150 + 136 151 ); 137 152 } 138 153
+162
src/applications/phortune/controller/PhortuneProductEditController.php
··· 1 + <?php 2 + 3 + final class PhortuneProductEditController extends PhabricatorController { 4 + 5 + private $productID; 6 + 7 + public function willProcessRequest(array $data) { 8 + $this->productID = idx($data, 'id'); 9 + } 10 + 11 + public function processRequest() { 12 + $request = $this->getRequest(); 13 + $user = $request->getUser(); 14 + 15 + if ($this->productID) { 16 + $product = id(new PhortuneProductQuery()) 17 + ->setViewer($user) 18 + ->withIDs(array($this->productID)) 19 + ->executeOne(); 20 + if (!$product) { 21 + return new Aphront404Response(); 22 + } 23 + 24 + $is_create = false; 25 + $cancel_uri = $this->getApplicationURI( 26 + 'product/view/'.$this->productID.'/'); 27 + } else { 28 + $product = new PhortuneProduct(); 29 + $is_create = true; 30 + $cancel_uri = $this->getApplicationURI('product/'); 31 + } 32 + 33 + $v_name = $product->getProductName(); 34 + $v_type = $product->getProductType(); 35 + $v_price = (int)$product->getPriceInCents(); 36 + $display_price = PhortuneUtil::formatCurrency($v_price); 37 + 38 + $e_name = true; 39 + $e_type = null; 40 + $e_price = true; 41 + $errors = array(); 42 + 43 + if ($request->isFormPost()) { 44 + $v_name = $request->getStr('name'); 45 + if (!strlen($v_name)) { 46 + $e_name = pht('Required'); 47 + $errors[] = pht('Product must have a name.'); 48 + } else { 49 + $e_name = null; 50 + } 51 + 52 + if ($is_create) { 53 + $v_type = $request->getStr('type'); 54 + $type_map = PhortuneProduct::getTypeMap(); 55 + if (empty($type_map[$v_type])) { 56 + $e_type = pht('Invalid'); 57 + $errors[] = pht('Product type is invalid.'); 58 + } else { 59 + $e_type = null; 60 + } 61 + } 62 + 63 + $display_price = $request->getStr('price'); 64 + try { 65 + $v_price = PhortuneUtil::parseCurrency($display_price); 66 + $e_price = null; 67 + } catch (Exception $ex) { 68 + $errors[] = pht('Price should be formatted as: $1.23'); 69 + $e_price = pht('Invalid'); 70 + } 71 + 72 + if (!$errors) { 73 + $xactions = array(); 74 + 75 + $xactions[] = id(new PhortuneProductTransaction()) 76 + ->setTransactionType(PhortuneProductTransaction::TYPE_NAME) 77 + ->setNewValue($v_name); 78 + 79 + $xactions[] = id(new PhortuneProductTransaction()) 80 + ->setTransactionType(PhortuneProductTransaction::TYPE_TYPE) 81 + ->setNewValue($v_type); 82 + 83 + $xactions[] = id(new PhortuneProductTransaction()) 84 + ->setTransactionType(PhortuneProductTransaction::TYPE_PRICE) 85 + ->setNewValue($v_price); 86 + 87 + $editor = id(new PhortuneProductEditor()) 88 + ->setActor($user) 89 + ->setContinueOnNoEffect(true) 90 + ->setContentSourceFromRequest($request); 91 + 92 + $editor->applyTransactions($product, $xactions); 93 + 94 + return id(new AphrontRedirectResponse())->setURI( 95 + $this->getApplicationURI('product/view/'.$product->getID().'/')); 96 + } 97 + } 98 + 99 + if ($errors) { 100 + $errors = id(new AphrontErrorView()) 101 + ->setErrors($errors); 102 + } 103 + 104 + $form = id(new AphrontFormView()) 105 + ->setUser($user) 106 + ->appendChild( 107 + id(new AphrontFormTextControl()) 108 + ->setLabel(pht('Name')) 109 + ->setName('name') 110 + ->setValue($v_name) 111 + ->setError($e_name)) 112 + ->appendChild( 113 + id(new AphrontFormSelectControl()) 114 + ->setLabel(pht('Type')) 115 + ->setName('type') 116 + ->setValue($v_type) 117 + ->setError($e_type) 118 + ->setOptions(PhortuneProduct::getTypeMap()) 119 + ->setDisabled(!$is_create)) 120 + ->appendChild( 121 + id(new AphrontFormTextControl()) 122 + ->setLabel(pht('Price')) 123 + ->setName('price') 124 + ->setValue($display_price) 125 + ->setError($e_price)) 126 + ->appendChild( 127 + id(new AphrontFormSubmitControl()) 128 + ->setValue( 129 + $is_create 130 + ? pht('Create Product') 131 + : pht('Save Product')) 132 + ->addCancelButton($cancel_uri)); 133 + 134 + $title = pht('Edit Product'); 135 + $crumbs = $this->buildApplicationCrumbs(); 136 + $crumbs->addCrumb( 137 + id(new PhabricatorCrumbView()) 138 + ->setName(pht('Products')) 139 + ->setHref($this->getApplicationURI('product/'))); 140 + $crumbs->addCrumb( 141 + id(new PhabricatorCrumbView()) 142 + ->setName($is_create ? pht('Create') : pht('Edit')) 143 + ->setHref($request->getRequestURI())); 144 + 145 + $header = id(new PhabricatorHeaderView()) 146 + ->setHeader(pht('Edit Product')); 147 + 148 + return $this->buildApplicationPage( 149 + array( 150 + $crumbs, 151 + $header, 152 + $errors, 153 + $form, 154 + ), 155 + array( 156 + 'title' => $title, 157 + 'device' => true, 158 + 'dust' => true, 159 + )); 160 + } 161 + 162 + }
+67
src/applications/phortune/controller/PhortuneProductListController.php
··· 1 + <?php 2 + 3 + final class PhortuneProductListController extends PhabricatorController { 4 + 5 + public function processRequest() { 6 + $request = $this->getRequest(); 7 + $user = $request->getUser(); 8 + 9 + $pager = new AphrontCursorPagerView(); 10 + $pager->readFromRequest($request); 11 + 12 + $query = id(new PhortuneProductQuery()) 13 + ->setViewer($user); 14 + 15 + $products = $query->executeWithCursorPager($pager); 16 + 17 + $title = pht('Product List'); 18 + 19 + $crumbs = $this->buildApplicationCrumbs(); 20 + $crumbs->addCrumb( 21 + id(new PhabricatorCrumbView()) 22 + ->setName('Products') 23 + ->setHref($this->getApplicationURI('product/'))); 24 + $crumbs->addAction( 25 + id(new PhabricatorMenuItemView()) 26 + ->setName(pht('Create Product')) 27 + ->setHref($this->getApplicationURI('product/edit/')) 28 + ->setIcon('create')); 29 + 30 + $header = id(new PhabricatorHeaderView()) 31 + ->setHeader(pht('Product List')); 32 + 33 + $product_list = id(new PhabricatorObjectItemListView()) 34 + ->setUser($user) 35 + ->setNoDataString(pht('No products.')); 36 + 37 + foreach ($products as $product) { 38 + $view_uri = $this->getApplicationURI( 39 + 'product/view/'.$product->getID().'/'); 40 + 41 + $price = $product->getPriceInCents(); 42 + 43 + $item = id(new PhabricatorObjectItemView()) 44 + ->setObjectName($product->getID()) 45 + ->setHeader($product->getProductName()) 46 + ->setHref($view_uri) 47 + ->addAttribute(PhortuneUtil::formatCurrency($price)) 48 + ->addAttribute($product->getTypeName()); 49 + 50 + $product_list->addItem($item); 51 + } 52 + 53 + return $this->buildApplicationPage( 54 + array( 55 + $crumbs, 56 + $header, 57 + $product_list, 58 + $pager, 59 + ), 60 + array( 61 + 'title' => $title, 62 + 'device' => true, 63 + 'dust' => true, 64 + )); 65 + } 66 + 67 + }
+84
src/applications/phortune/controller/PhortuneProductViewController.php
··· 1 + <?php 2 + 3 + final class PhortuneProductViewController extends PhabricatorController { 4 + 5 + private $productID; 6 + 7 + public function willProcessRequest(array $data) { 8 + $this->productID = $data['id']; 9 + } 10 + 11 + public function processRequest() { 12 + $request = $this->getRequest(); 13 + $user = $request->getUser(); 14 + 15 + $product = id(new PhortuneProductQuery()) 16 + ->setViewer($user) 17 + ->withIDs(array($this->productID)) 18 + ->executeOne(); 19 + 20 + if (!$product) { 21 + return new Aphront404Response(); 22 + } 23 + 24 + $title = pht('Product: %s', $product->getProductName()); 25 + 26 + $crumbs = $this->buildApplicationCrumbs(); 27 + $crumbs->addCrumb( 28 + id(new PhabricatorCrumbView()) 29 + ->setName(pht('Products')) 30 + ->setHref($this->getApplicationURI('product/'))); 31 + $crumbs->addCrumb( 32 + id(new PhabricatorCrumbView()) 33 + ->setName(pht('#%d', $product->getID())) 34 + ->setHref($request->getRequestURI())); 35 + 36 + $header = id(new PhabricatorHeaderView()) 37 + ->setHeader($product->getProductName()); 38 + 39 + $edit_uri = $this->getApplicationURI('product/edit/'.$product->getID().'/'); 40 + 41 + $actions = id(new PhabricatorActionListView()) 42 + ->setUser($user) 43 + ->addAction( 44 + id(new PhabricatorActionView()) 45 + ->setName(pht('Edit Product')) 46 + ->setHref($edit_uri) 47 + ->setIcon('edit')); 48 + 49 + $properties = id(new PhabricatorPropertyListView()) 50 + ->setUser($user) 51 + ->addProperty(pht('Type'), $product->getTypeName()) 52 + ->addProperty( 53 + pht('Price'), 54 + PhortuneUtil::formatCurrency($product->getPriceInCents())); 55 + 56 + $xactions = id(new PhortuneProductTransactionQuery()) 57 + ->setViewer($user) 58 + ->withObjectPHIDs(array($product->getPHID())) 59 + ->execute(); 60 + 61 + $engine = id(new PhabricatorMarkupEngine()) 62 + ->setViewer($user); 63 + 64 + $xaction_view = id(new PhabricatorApplicationTransactionView()) 65 + ->setUser($user) 66 + ->setTransactions($xactions) 67 + ->setMarkupEngine($engine); 68 + 69 + return $this->buildApplicationPage( 70 + array( 71 + $crumbs, 72 + $header, 73 + $actions, 74 + $properties, 75 + $xaction_view, 76 + ), 77 + array( 78 + 'title' => $title, 79 + 'device' => true, 80 + 'dust' => true, 81 + )); 82 + } 83 + 84 + }
+73
src/applications/phortune/editor/PhortuneProductEditor.php
··· 1 + <?php 2 + 3 + 4 + final class PhortuneProductEditor 5 + extends PhabricatorApplicationTransactionEditor { 6 + 7 + public function getTransactionTypes() { 8 + $types = parent::getTransactionTypes(); 9 + 10 + $types[] = PhortuneProductTransaction::TYPE_NAME; 11 + $types[] = PhortuneProductTransaction::TYPE_TYPE; 12 + $types[] = PhortuneProductTransaction::TYPE_PRICE; 13 + 14 + return $types; 15 + } 16 + 17 + 18 + protected function getCustomTransactionOldValue( 19 + PhabricatorLiskDAO $object, 20 + PhabricatorApplicationTransaction $xaction) { 21 + switch ($xaction->getTransactionType()) { 22 + case PhortuneProductTransaction::TYPE_NAME: 23 + return $object->getProductName(); 24 + case PhortuneProductTransaction::TYPE_TYPE: 25 + return $object->getProductType(); 26 + case PhortuneProductTransaction::TYPE_PRICE: 27 + return $object->getPriceInCents(); 28 + } 29 + return parent::getCustomTransactionOldValue($object, $xaction); 30 + } 31 + 32 + protected function getCustomTransactionNewValue( 33 + PhabricatorLiskDAO $object, 34 + PhabricatorApplicationTransaction $xaction) { 35 + switch ($xaction->getTransactionType()) { 36 + case PhortuneProductTransaction::TYPE_NAME: 37 + case PhortuneProductTransaction::TYPE_TYPE: 38 + case PhortuneProductTransaction::TYPE_PRICE: 39 + return $xaction->getNewValue(); 40 + } 41 + return parent::getCustomTransactionNewValue($object, $xaction); 42 + } 43 + 44 + protected function applyCustomInternalTransaction( 45 + PhabricatorLiskDAO $object, 46 + PhabricatorApplicationTransaction $xaction) { 47 + switch ($xaction->getTransactionType()) { 48 + case PhortuneProductTransaction::TYPE_NAME: 49 + $object->setProductName($xaction->getNewValue()); 50 + return; 51 + case PhortuneProductTransaction::TYPE_TYPE: 52 + $object->setProductType($xaction->getNewValue()); 53 + return; 54 + case PhortuneProductTransaction::TYPE_PRICE: 55 + $object->setPriceInCents($xaction->getNewValue()); 56 + return; 57 + } 58 + return parent::applyCustomInternalTransaction($object, $xaction); 59 + } 60 + 61 + protected function applyCustomExternalTransaction( 62 + PhabricatorLiskDAO $object, 63 + PhabricatorApplicationTransaction $xaction) { 64 + switch ($xaction->getTransactionType()) { 65 + case PhortuneProductTransaction::TYPE_NAME: 66 + case PhortuneProductTransaction::TYPE_TYPE: 67 + case PhortuneProductTransaction::TYPE_PRICE: 68 + return; 69 + } 70 + return parent::applyCustomExternalTransaction($object, $xaction); 71 + } 72 + 73 + }
+56
src/applications/phortune/query/PhortuneProductQuery.php
··· 1 + <?php 2 + 3 + final class PhortuneProductQuery 4 + extends PhabricatorCursorPagedPolicyAwareQuery { 5 + 6 + private $ids; 7 + private $phids; 8 + 9 + public function withIDs(array $ids) { 10 + $this->ids = $ids; 11 + return $this; 12 + } 13 + 14 + public function withPHIDs(array $phids) { 15 + $this->phids = $phids; 16 + return $this; 17 + } 18 + 19 + protected function loadPage() { 20 + $table = new PhortuneProduct(); 21 + $conn = $table->establishConnection('r'); 22 + 23 + $rows = queryfx_all( 24 + $conn, 25 + 'SELECT * FROM %T %Q %Q %Q', 26 + $table->getTableName(), 27 + $this->buildWhereClause($conn), 28 + $this->buildOrderClause($conn), 29 + $this->buildLimitClause($conn)); 30 + 31 + return $table->loadAllFromArray($rows); 32 + } 33 + 34 + private function buildWhereClause(AphrontDatabaseConnection $conn) { 35 + $where = array(); 36 + 37 + if ($this->ids) { 38 + $where[] = qsprintf( 39 + $conn, 40 + 'id IN (%Ld)', 41 + $this->ids); 42 + } 43 + 44 + if ($this->phids) { 45 + $where[] = qsprintf( 46 + $conn, 47 + 'phid IN (%Ls)', 48 + $this->phids); 49 + } 50 + 51 + $where[] = $this->buildPagingClause($conn); 52 + 53 + return $this->formatWhereClause($where); 54 + } 55 + 56 + }
+10
src/applications/phortune/query/PhortuneProductTransactionQuery.php
··· 1 + <?php 2 + 3 + final class PhortuneProductTransactionQuery 4 + extends PhabricatorApplicationTransactionQuery { 5 + 6 + protected function getTemplateApplicationTransaction() { 7 + return new PhortuneProductTransaction(); 8 + } 9 + 10 + }
+45 -3
src/applications/phortune/storage/PhortuneProduct.php
··· 4 4 * A product is something users can purchase. It may be a one-time purchase, 5 5 * or a plan which is billed monthly. 6 6 */ 7 - final class PhortuneProduct extends PhortuneDAO { 7 + final class PhortuneProduct extends PhortuneDAO 8 + implements PhabricatorPolicyInterface { 8 9 9 - const TYPE_BILL_ONCE = 'phortune:thing'; 10 - const TYPE_BILL_PLAN = 'phortune:plan'; 10 + const TYPE_BILL_ONCE = 'phortune:thing'; 11 + const TYPE_BILL_PLAN = 'phortune:plan'; 12 + 13 + const STATUS_ACTIVE = 'product:active'; 14 + const STATUS_DISABLED = 'product:disabled'; 11 15 12 16 protected $productName; 17 + protected $productType; 18 + protected $status = self::STATUS_ACTIVE; 13 19 protected $priceInCents; 14 20 protected $billingIntervalInMonths; 15 21 protected $trialPeriodInDays; ··· 29 35 PhabricatorPHIDConstants::PHID_TYPE_PDCT); 30 36 } 31 37 38 + public static function getTypeMap() { 39 + return array( 40 + self::TYPE_BILL_ONCE => pht('Product (Charged Once)'), 41 + self::TYPE_BILL_PLAN => pht('Flat Rate Plan (Charged Monthly)'), 42 + ); 43 + } 32 44 45 + public function getTypeName() { 46 + return idx(self::getTypeMap(), $this->getProductType()); 47 + } 48 + 49 + public function getPriceInCents() { 50 + $price = parent::getPriceInCents(); 51 + if ($price === null) { 52 + return $price; 53 + } else { 54 + return (int)parent::getPriceInCents(); 55 + } 56 + } 57 + 58 + 59 + /* -( PhabricatorPolicyInterface )----------------------------------------- */ 60 + 61 + 62 + public function getCapabilities() { 63 + return array( 64 + PhabricatorPolicyCapability::CAN_VIEW, 65 + ); 66 + } 67 + 68 + public function getPolicy($capability) { 69 + return PhabricatorPolicies::POLICY_USER; 70 + } 71 + 72 + public function hasAutomaticCapability($capability, PhabricatorUser $viewer) { 73 + return false; 74 + } 33 75 34 76 }
+80
src/applications/phortune/storage/PhortuneProductTransaction.php
··· 1 + <?php 2 + 3 + final class PhortuneProductTransaction 4 + extends PhabricatorApplicationTransaction { 5 + 6 + const TYPE_NAME = 'product:name'; 7 + const TYPE_TYPE = 'product:type'; 8 + const TYPE_PRICE = 'product:price'; 9 + 10 + public function getApplicationName() { 11 + return 'phortune'; 12 + } 13 + 14 + public function getApplicationTransactionType() { 15 + return PhabricatorPHIDConstants::PHID_TYPE_PDCT; 16 + } 17 + 18 + public function getApplicationTransactionCommentObject() { 19 + return null; 20 + } 21 + 22 + public function getApplicationObjectTypeName() { 23 + return pht('product'); 24 + } 25 + 26 + public function getTitle() { 27 + $author_phid = $this->getAuthorPHID(); 28 + 29 + $old = $this->getOldValue(); 30 + $new = $this->getNewValue(); 31 + 32 + switch ($this->getTransactionType()) { 33 + case self::TYPE_NAME: 34 + if ($old === null) { 35 + return pht( 36 + '%s created this product.', 37 + $this->renderHandleLink($author_phid)); 38 + } else { 39 + return pht( 40 + '%s renamed this product from "%s" to "%s".', 41 + $this->renderHandleLink($author_phid), 42 + $old, 43 + $new); 44 + } 45 + break; 46 + case self::TYPE_PRICE: 47 + if ($old === null) { 48 + return pht( 49 + '%s set product price to %s.', 50 + $this->renderHandleLink($author_phid), 51 + PhortuneUtil::formatCurrency($new)); 52 + } else { 53 + return pht( 54 + '%s changed product price from %s to %s.', 55 + $this->renderHandleLink($author_phid), 56 + PhortuneUtil::formatCurrency($old), 57 + PhortuneUtil::formatCurrency($new)); 58 + } 59 + break; 60 + case self::TYPE_TYPE: 61 + $map = PhortuneProduct::getTypeMap(); 62 + if ($old === null) { 63 + return pht( 64 + '%s set product type to "%s".', 65 + $this->renderHandleLink($author_phid), 66 + $map[$new]); 67 + } else { 68 + return pht( 69 + '%s changed product type from "%s" to "%s".', 70 + $this->renderHandleLink($author_phid), 71 + $map[$old], 72 + $map[$new]); 73 + } 74 + break; 75 + } 76 + 77 + return parent::getTitle(); 78 + } 79 + 80 + }
+8
src/applications/phortune/storage/PhortunePurchase.php
··· 5 5 */ 6 6 final class PhortunePurchase extends PhortuneDAO { 7 7 8 + const STATUS_PENDING = 'purchase:pending'; 8 9 const STATUS_PROCESSING = 'purchase:processing'; 9 10 const STATUS_ACTIVE = 'purchase:active'; 10 11 const STATUS_CANCELED = 'purchase:canceled'; ··· 14 15 protected $productPHID; 15 16 protected $accountPHID; 16 17 protected $authorPHID; 18 + protected $purchaseName; 19 + protected $purchaseURI; 17 20 protected $paymentMethodPHID; 21 + protected $basePriceInCents; 22 + protected $priceAdjustmentInCents; 23 + protected $finalPriceInCents; 18 24 protected $quantity; 25 + protected $totalPriceInCents; 19 26 protected $status; 27 + protected $metadata; 20 28 21 29 public function getConfiguration() { 22 30 return array(
+34
src/applications/phortune/util/PhortuneUtil.php
··· 1 + <?php 2 + 3 + final class PhortuneUtil { 4 + 5 + public static function parseCurrency($string) { 6 + $string = trim($string); 7 + if (!preg_match('/^[-]?[$]?\d+([.]\d{0,2})?$/', $string)) { 8 + throw new Exception("Invalid currency format ('{$string}')."); 9 + } 10 + 11 + $value = str_replace('$', '', $string); 12 + $value = (float)$value; 13 + $value = (int)round(100 * $value); 14 + 15 + return $value; 16 + } 17 + 18 + public static function formatCurrency($price_in_cents) { 19 + if (!preg_match('/^[-]?\d+$/', $price_in_cents)) { 20 + throw new Exception("Invalid price in cents ('{$price_in_cents}')."); 21 + } 22 + 23 + $negative = ($price_in_cents < 0); 24 + $price_in_cents = abs($price_in_cents); 25 + $display_value = sprintf('$%.02f', $price_in_cents / 100); 26 + 27 + if ($negative) { 28 + $display_value = '-'.$display_value; 29 + } 30 + 31 + return $display_value; 32 + } 33 + 34 + }
+9
src/applications/transactions/editor/PhabricatorApplicationTransactionEditor.php
··· 262 262 return $this; 263 263 } 264 264 265 + public function setContentSourceFromRequest(AphrontRequest $request) { 266 + return $this->setContentSource( 267 + PhabricatorContentSource::newForSource( 268 + PhabricatorContentSource::SOURCE_WEB, 269 + array( 270 + 'ip' => $request->getRemoteAddr(), 271 + ))); 272 + } 273 + 265 274 public function getContentSource() { 266 275 return $this->contentSource; 267 276 }
+4
src/infrastructure/storage/patch/PhabricatorBuiltinPatchList.php
··· 1210 1210 'type' => 'sql', 1211 1211 'name' => $this->getPatchPath('20130323.phortunepayment.sql'), 1212 1212 ), 1213 + '20130324.phortuneproduct.sql' => array( 1214 + 'type' => 'sql', 1215 + 'name' => $this->getPatchPath('20130324.phortuneproduct.sql'), 1216 + ), 1213 1217 ); 1214 1218 } 1215 1219