Skip to content
This repository
Browse code

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
  • Loading branch information...
commit abdaf902395132ad2d5de531e8b86b136e4bcfb7 1 parent 4f3b5f0
Evan Priestley authored March 28, 2013
35  resources/sql/patches/20130324.phortuneproduct.sql
... ...
@@ -0,0 +1,35 @@
  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;
83  src/__celerity_resource_map__.php
@@ -2138,14 +2138,15 @@
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,7 +2884,7 @@
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,7 +3705,7 @@
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,7 +3747,7 @@
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,16 +3938,16 @@
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,7 +3961,7 @@
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,48 +4033,48 @@
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
 ));
21  src/__phutil_library_map__.php
@@ -1546,8 +1546,16 @@
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,7 +3208,18 @@
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,6 +43,11 @@ public function getRoutes() {
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,6 +54,7 @@ public function processRequest() {
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,6 +64,7 @@ public function processRequest() {
63 64
         $actions,
64 65
         $properties,
65 66
         $payment_methods,
  67
+        $purchase_history,
66 68
         $account_history,
67 69
       ),
68 70
       array(
@@ -136,6 +138,19 @@ private function buildPaymentMethodsSection(PhortuneAccount $account) {
136 138
     );
137 139
   }
138 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
+
  151
+    );
  152
+  }
  153
+
139 154
   private function buildAccountHistorySection(PhortuneAccount $account) {
140 155
     $request = $this->getRequest();
141 156
     $user = $request->getUser();
162  src/applications/phortune/controller/PhortuneProductEditController.php
... ...
@@ -0,0 +1,162 @@
  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
... ...
@@ -0,0 +1,67 @@
  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
... ...
@@ -0,0 +1,84 @@
  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
... ...
@@ -0,0 +1,73 @@
  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
... ...
@@ -0,0 +1,56 @@
  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
... ...
@@ -0,0 +1,10 @@
  1
+<?php
  2
+
  3
+final class PhortuneProductTransactionQuery
  4
+  extends PhabricatorApplicationTransactionQuery {
  5
+
  6
+  protected function getTemplateApplicationTransaction() {
  7
+    return new PhortuneProductTransaction();
  8
+  }
  9
+
  10
+}
48  src/applications/phortune/storage/PhortuneProduct.php
@@ -4,12 +4,18 @@
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,6 +35,42 @@ public function generatePHID() {
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
+  }
  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
+
32 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
... ...
@@ -0,0 +1,80 @@
  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,6 +5,7 @@
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,9 +15,16 @@
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
... ...
@@ -0,0 +1,34 @@
  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,6 +262,15 @@ public function setContentSource(PhabricatorContentSource $content_source) {
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,6 +1210,10 @@ public function getPatches() {
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
 

0 notes on commit abdaf90

Please sign in to comment.
Something went wrong with that request. Please try again.