Page Menu
Home
Phabricator
Search
Configure Global Search
Log In
Files
F14091385
D20721.diff
No One
Temporary
Actions
View File
Edit File
Delete File
View Transforms
Subscribe
Mute Notifications
Award Token
Flag For Later
Size
45 KB
Referenced Files
None
Subscribers
None
D20721.diff
View Options
diff --git a/resources/sql/autopatches/20190816.subscription.01.xaction.sql b/resources/sql/autopatches/20190816.subscription.01.xaction.sql
new file mode 100644
--- /dev/null
+++ b/resources/sql/autopatches/20190816.subscription.01.xaction.sql
@@ -0,0 +1,19 @@
+CREATE TABLE {$NAMESPACE}_phortune.phortune_subscriptiontransaction (
+ id INT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
+ phid VARBINARY(64) NOT NULL,
+ authorPHID VARBINARY(64) NOT NULL,
+ objectPHID VARBINARY(64) NOT NULL,
+ viewPolicy VARBINARY(64) NOT NULL,
+ editPolicy VARBINARY(64) NOT NULL,
+ commentPHID VARBINARY(64) DEFAULT NULL,
+ commentVersion INT UNSIGNED NOT NULL,
+ transactionType VARCHAR(32) NOT NULL,
+ oldValue LONGTEXT NOT NULL,
+ newValue LONGTEXT NOT NULL,
+ contentSource LONGTEXT NOT NULL,
+ metadata LONGTEXT NOT NULL,
+ dateCreated INT UNSIGNED NOT NULL,
+ dateModified INT UNSIGNED NOT NULL,
+ UNIQUE KEY `key_phid` (`phid`),
+ KEY `key_object` (`objectPHID`)
+) ENGINE=InnoDB DEFAULT CHARSET={$CHARSET} COLLATE {$COLLATE_TEXT};
diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php
--- a/src/__phutil_library_map__.php
+++ b/src/__phutil_library_map__.php
@@ -5249,11 +5249,13 @@
'PhortuneAccountOrdersController' => 'applications/phortune/controller/account/PhortuneAccountOrdersController.php',
'PhortuneAccountOverviewController' => 'applications/phortune/controller/account/PhortuneAccountOverviewController.php',
'PhortuneAccountPHIDType' => 'applications/phortune/phid/PhortuneAccountPHIDType.php',
- 'PhortuneAccountPaymentMethodListController' => 'applications/phortune/controller/account/PhortuneAccountPaymentMethodListController.php',
+ 'PhortuneAccountPaymentMethodController' => 'applications/phortune/controller/account/PhortuneAccountPaymentMethodController.php',
'PhortuneAccountPaymentMethodViewController' => 'applications/phortune/controller/account/PhortuneAccountPaymentMethodViewController.php',
'PhortuneAccountProfileController' => 'applications/phortune/controller/account/PhortuneAccountProfileController.php',
'PhortuneAccountQuery' => 'applications/phortune/query/PhortuneAccountQuery.php',
+ 'PhortuneAccountSubscriptionAutopayController' => 'applications/phortune/controller/account/PhortuneAccountSubscriptionAutopayController.php',
'PhortuneAccountSubscriptionController' => 'applications/phortune/controller/account/PhortuneAccountSubscriptionController.php',
+ 'PhortuneAccountSubscriptionViewController' => 'applications/phortune/controller/account/PhortuneAccountSubscriptionViewController.php',
'PhortuneAccountTransaction' => 'applications/phortune/storage/PhortuneAccountTransaction.php',
'PhortuneAccountTransactionQuery' => 'applications/phortune/query/PhortuneAccountTransactionQuery.php',
'PhortuneAccountTransactionType' => 'applications/phortune/xaction/PhortuneAccountTransactionType.php',
@@ -5361,16 +5363,21 @@
'PhortuneSchemaSpec' => 'applications/phortune/storage/PhortuneSchemaSpec.php',
'PhortuneStripePaymentProvider' => 'applications/phortune/provider/PhortuneStripePaymentProvider.php',
'PhortuneSubscription' => 'applications/phortune/storage/PhortuneSubscription.php',
+ 'PhortuneSubscriptionAutopayTransaction' => 'applications/phortune/xaction/subscription/PhortuneSubscriptionAutopayTransaction.php',
'PhortuneSubscriptionCart' => 'applications/phortune/cart/PhortuneSubscriptionCart.php',
'PhortuneSubscriptionEditController' => 'applications/phortune/controller/subscription/PhortuneSubscriptionEditController.php',
+ 'PhortuneSubscriptionEditor' => 'applications/phortune/editor/PhortuneSubscriptionEditor.php',
'PhortuneSubscriptionImplementation' => 'applications/phortune/subscription/PhortuneSubscriptionImplementation.php',
'PhortuneSubscriptionListController' => 'applications/phortune/controller/subscription/PhortuneSubscriptionListController.php',
'PhortuneSubscriptionPHIDType' => 'applications/phortune/phid/PhortuneSubscriptionPHIDType.php',
+ 'PhortuneSubscriptionPolicyCodex' => 'applications/phortune/codex/PhortuneSubscriptionPolicyCodex.php',
'PhortuneSubscriptionProduct' => 'applications/phortune/product/PhortuneSubscriptionProduct.php',
'PhortuneSubscriptionQuery' => 'applications/phortune/query/PhortuneSubscriptionQuery.php',
'PhortuneSubscriptionSearchEngine' => 'applications/phortune/query/PhortuneSubscriptionSearchEngine.php',
'PhortuneSubscriptionTableView' => 'applications/phortune/view/PhortuneSubscriptionTableView.php',
- 'PhortuneSubscriptionViewController' => 'applications/phortune/controller/subscription/PhortuneSubscriptionViewController.php',
+ 'PhortuneSubscriptionTransaction' => 'applications/phortune/storage/PhortuneSubscriptionTransaction.php',
+ 'PhortuneSubscriptionTransactionQuery' => 'applications/phortune/query/PhortuneSubscriptionTransactionQuery.php',
+ 'PhortuneSubscriptionTransactionType' => 'applications/phortune/xaction/subscription/PhortuneSubscriptionTransactionType.php',
'PhortuneSubscriptionWorker' => 'applications/phortune/worker/PhortuneSubscriptionWorker.php',
'PhortuneTestPaymentProvider' => 'applications/phortune/provider/PhortuneTestPaymentProvider.php',
'PhragmentBrowseController' => 'applications/phragment/controller/PhragmentBrowseController.php',
@@ -11812,11 +11819,13 @@
'PhortuneAccountOrdersController' => 'PhortuneAccountProfileController',
'PhortuneAccountOverviewController' => 'PhortuneAccountProfileController',
'PhortuneAccountPHIDType' => 'PhabricatorPHIDType',
- 'PhortuneAccountPaymentMethodListController' => 'PhortuneAccountProfileController',
+ 'PhortuneAccountPaymentMethodController' => 'PhortuneAccountProfileController',
'PhortuneAccountPaymentMethodViewController' => 'PhortuneAccountController',
'PhortuneAccountProfileController' => 'PhortuneAccountController',
'PhortuneAccountQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
+ 'PhortuneAccountSubscriptionAutopayController' => 'PhortuneAccountController',
'PhortuneAccountSubscriptionController' => 'PhortuneAccountProfileController',
+ 'PhortuneAccountSubscriptionViewController' => 'PhortuneAccountController',
'PhortuneAccountTransaction' => 'PhabricatorModularTransaction',
'PhortuneAccountTransactionQuery' => 'PhabricatorApplicationTransactionQuery',
'PhortuneAccountTransactionType' => 'PhabricatorModularTransactionType',
@@ -11953,17 +11962,25 @@
'PhortuneSubscription' => array(
'PhortuneDAO',
'PhabricatorPolicyInterface',
+ 'PhabricatorExtendedPolicyInterface',
+ 'PhabricatorPolicyCodexInterface',
+ 'PhabricatorApplicationTransactionInterface',
),
+ 'PhortuneSubscriptionAutopayTransaction' => 'PhortuneSubscriptionTransactionType',
'PhortuneSubscriptionCart' => 'PhortuneCartImplementation',
'PhortuneSubscriptionEditController' => 'PhortuneController',
+ 'PhortuneSubscriptionEditor' => 'PhabricatorApplicationTransactionEditor',
'PhortuneSubscriptionImplementation' => 'Phobject',
'PhortuneSubscriptionListController' => 'PhortuneController',
'PhortuneSubscriptionPHIDType' => 'PhabricatorPHIDType',
+ 'PhortuneSubscriptionPolicyCodex' => 'PhabricatorPolicyCodex',
'PhortuneSubscriptionProduct' => 'PhortuneProductImplementation',
'PhortuneSubscriptionQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
'PhortuneSubscriptionSearchEngine' => 'PhabricatorApplicationSearchEngine',
'PhortuneSubscriptionTableView' => 'AphrontView',
- 'PhortuneSubscriptionViewController' => 'PhortuneController',
+ 'PhortuneSubscriptionTransaction' => 'PhabricatorModularTransaction',
+ 'PhortuneSubscriptionTransactionQuery' => 'PhabricatorApplicationTransactionQuery',
+ 'PhortuneSubscriptionTransactionType' => 'PhabricatorModularTransactionType',
'PhortuneSubscriptionWorker' => 'PhabricatorWorker',
'PhortuneTestPaymentProvider' => 'PhortunePaymentProvider',
'PhragmentBrowseController' => 'PhragmentController',
diff --git a/src/applications/phortune/application/PhabricatorPhortuneApplication.php b/src/applications/phortune/application/PhabricatorPhortuneApplication.php
--- a/src/applications/phortune/application/PhabricatorPhortuneApplication.php
+++ b/src/applications/phortune/application/PhabricatorPhortuneApplication.php
@@ -43,9 +43,7 @@
'(?:query/(?P<queryKey>[^/]+)/)?'
=> 'PhortuneSubscriptionListController',
'view/(?P<id>\d+)/'
- => 'PhortuneSubscriptionViewController',
- 'edit/(?P<id>\d+)/'
- => 'PhortuneSubscriptionEditController',
+ => 'PhortuneAccountSubscriptionViewController',
'order/(?P<subscriptionID>\d+)/'
=> 'PhortuneCartListController',
),
@@ -73,12 +71,18 @@
'(?P<accountID>\d+)/' => array(
'details/' => 'PhortuneAccountDetailsController',
'methods/' => array(
- '' => 'PhortuneAccountPaymentMethodListController',
+ '' => 'PhortuneAccountPaymentMethodController',
'(?P<id>\d+)/' => 'PhortuneAccountPaymentMethodViewController',
),
'orders/' => 'PhortuneAccountOrdersController',
'charges/' => 'PhortuneAccountChargesController',
- 'subscriptions/' => 'PhortuneAccountSubscriptionController',
+ 'subscriptions/' => array(
+ '' => 'PhortuneAccountSubscriptionController',
+ '(?P<subscriptionID>\d+)/' => array(
+ 'autopay/(?P<methodID>\d+)/'
+ => 'PhortuneAccountSubscriptionAutopayController',
+ ),
+ ),
'managers/' => array(
'' => 'PhortuneAccountManagersController',
'add/' => 'PhortuneAccountAddManagerController',
@@ -124,7 +128,7 @@
'(?:query/(?P<queryKey>[^/]+)/)?'
=> 'PhortuneSubscriptionListController',
'view/(?P<id>\d+)/'
- => 'PhortuneSubscriptionViewController',
+ => 'PhortuneAccountSubscriptionViewController',
'order/(?P<subscriptionID>\d+)/'
=> 'PhortuneCartListController',
),
diff --git a/src/applications/phortune/codex/PhortunePaymentMethodPolicyCodex.php b/src/applications/phortune/codex/PhortunePaymentMethodPolicyCodex.php
--- a/src/applications/phortune/codex/PhortunePaymentMethodPolicyCodex.php
+++ b/src/applications/phortune/codex/PhortunePaymentMethodPolicyCodex.php
@@ -12,6 +12,7 @@
->setCapabilities(
array(
PhabricatorPolicyCapability::CAN_VIEW,
+ PhabricatorPolicyCapability::CAN_EDIT,
))
->setIsActive(true)
->setDescription(
diff --git a/src/applications/phortune/codex/PhortunePaymentMethodPolicyCodex.php b/src/applications/phortune/codex/PhortuneSubscriptionPolicyCodex.php
copy from src/applications/phortune/codex/PhortunePaymentMethodPolicyCodex.php
copy to src/applications/phortune/codex/PhortuneSubscriptionPolicyCodex.php
--- a/src/applications/phortune/codex/PhortunePaymentMethodPolicyCodex.php
+++ b/src/applications/phortune/codex/PhortuneSubscriptionPolicyCodex.php
@@ -1,6 +1,6 @@
<?php
-final class PhortunePaymentMethodPolicyCodex
+final class PhortuneSubscriptionPolicyCodex
extends PhabricatorPolicyCodex {
public function getPolicySpecialRuleDescriptions() {
@@ -12,11 +12,12 @@
->setCapabilities(
array(
PhabricatorPolicyCapability::CAN_VIEW,
+ PhabricatorPolicyCapability::CAN_EDIT,
))
->setIsActive(true)
->setDescription(
pht(
- 'Account members may view and edit payment methods.'));
+ 'Account members may view and edit subscriptions.'));
$rules[] = $this->newRule()
->setCapabilities(
@@ -27,7 +28,7 @@
->setDescription(
pht(
'Merchants you have a relationship with may view associated '.
- 'payment methods.'));
+ 'subscriptions.'));
return $rules;
}
diff --git a/src/applications/phortune/controller/account/PhortuneAccountPaymentMethodListController.php b/src/applications/phortune/controller/account/PhortuneAccountPaymentMethodController.php
rename from src/applications/phortune/controller/account/PhortuneAccountPaymentMethodListController.php
rename to src/applications/phortune/controller/account/PhortuneAccountPaymentMethodController.php
--- a/src/applications/phortune/controller/account/PhortuneAccountPaymentMethodListController.php
+++ b/src/applications/phortune/controller/account/PhortuneAccountPaymentMethodController.php
@@ -1,6 +1,6 @@
<?php
-final class PhortuneAccountPaymentMethodListController
+final class PhortuneAccountPaymentMethodController
extends PhortuneAccountProfileController {
protected function shouldRequireAccountEditCapability() {
diff --git a/src/applications/phortune/controller/account/PhortuneAccountSubscriptionAutopayController.php b/src/applications/phortune/controller/account/PhortuneAccountSubscriptionAutopayController.php
new file mode 100644
--- /dev/null
+++ b/src/applications/phortune/controller/account/PhortuneAccountSubscriptionAutopayController.php
@@ -0,0 +1,137 @@
+<?php
+
+final class PhortuneAccountSubscriptionAutopayController
+ extends PhortuneAccountController {
+
+ protected function shouldRequireAccountEditCapability() {
+ return true;
+ }
+
+ protected function handleAccountRequest(AphrontRequest $request) {
+ $viewer = $this->getViewer();
+ $account = $this->getAccount();
+
+ $subscription = id(new PhortuneSubscriptionQuery())
+ ->setViewer($viewer)
+ ->withIDs(array($request->getURIData('subscriptionID')))
+ ->withAccountPHIDs(array($account->getPHID()))
+ ->requireCapabilities(
+ array(
+ PhabricatorPolicyCapability::CAN_VIEW,
+ PhabricatorPolicyCapability::CAN_EDIT,
+ ))
+ ->executeOne();
+ if (!$subscription) {
+ return new Aphront404Response();
+ }
+
+ $method = id(new PhortunePaymentMethodQuery())
+ ->setViewer($viewer)
+ ->withIDs(array($request->getURIData('methodID')))
+ ->withAccountPHIDs(array($subscription->getAccountPHID()))
+ ->withMerchantPHIDs(array($subscription->getMerchantPHID()))
+ ->withStatuses(
+ array(
+ PhortunePaymentMethod::STATUS_ACTIVE,
+ ))
+ ->executeOne();
+ if (!$method) {
+ return new Aphront404Response();
+ }
+
+ $next_uri = $subscription->getURI();
+
+ $autopay_phid = $subscription->getDefaultPaymentMethodPHID();
+ $is_stop = ($autopay_phid === $method->getPHID());
+
+ if ($request->isFormOrHisecPost()) {
+ if ($is_stop) {
+ $new_phid = null;
+ } else {
+ $new_phid = $method->getPHID();
+ }
+
+ $xactions = array();
+
+ $xactions[] = $subscription->getApplicationTransactionTemplate()
+ ->setTransactionType(
+ PhortuneSubscriptionAutopayTransaction::TRANSACTIONTYPE)
+ ->setNewValue($new_phid);
+
+ $editor = $subscription->getApplicationTransactionEditor()
+ ->setActor($viewer)
+ ->setContentSourceFromRequest($request)
+ ->setContinueOnNoEffect(true)
+ ->setContinueOnMissingFields(true)
+ ->setCancelURI($next_uri);
+
+ $editor->applyTransactions($subscription, $xactions);
+
+ return id(new AphrontRedirectResponse())->setURI($next_uri);
+ }
+
+ $method_phid = $method->getPHID();
+ $subscription_phid = $subscription->getPHID();
+
+ $handles = $viewer->loadHandles(
+ array(
+ $method_phid,
+ $subscription_phid,
+ ));
+
+ $method_handle = $handles[$method_phid];
+ $subscription_handle = $handles[$subscription_phid];
+
+ $method_display = $method_handle->renderLink();
+ $method_display = phutil_tag(
+ 'strong',
+ array(),
+ $method_display);
+
+ $subscription_display = $subscription_handle->renderLink();
+ $subscription_display = phutil_tag(
+ 'strong',
+ array(),
+ $subscription_display);
+
+ $body = array();
+ if ($is_stop) {
+ $title = pht('Stop Autopay');
+
+ $body[] = pht(
+ 'Remove %s as the automatic payment method for subscription %s?',
+ $method_display,
+ $subscription_display);
+
+ $body[] = pht(
+ 'This payment method will no longer be charged automatically.');
+
+ $submit = pht('Stop Autopay');
+ } else {
+ $title = pht('Start Autopay');
+
+ $body[] = pht(
+ 'Set %s as the automatic payment method for subscription %s?',
+ $method_display,
+ $subscription_display);
+
+ $body[] = pht(
+ 'This payment method will be used to automatically pay future '.
+ 'charges.');
+
+ $submit = pht('Start Autopay');
+ }
+
+ $dialog = $this->newDialog()
+ ->setTitle($title)
+ ->addCancelButton($next_uri)
+ ->addSubmitButton($submit);
+
+ foreach ($body as $graph) {
+ $dialog->appendParagraph($graph);
+ }
+
+ return $dialog;
+ }
+
+}
diff --git a/src/applications/phortune/controller/account/PhortuneAccountSubscriptionViewController.php b/src/applications/phortune/controller/account/PhortuneAccountSubscriptionViewController.php
new file mode 100644
--- /dev/null
+++ b/src/applications/phortune/controller/account/PhortuneAccountSubscriptionViewController.php
@@ -0,0 +1,338 @@
+<?php
+
+final class PhortuneAccountSubscriptionViewController
+ extends PhortuneAccountController {
+
+ protected function shouldRequireAccountEditCapability() {
+ return false;
+ }
+
+ protected function handleAccountRequest(AphrontRequest $request) {
+ $viewer = $this->getViewer();
+
+ $subscription = id(new PhortuneSubscriptionQuery())
+ ->setViewer($viewer)
+ ->withIDs(array($request->getURIData('id')))
+ ->needTriggers(true)
+ ->executeOne();
+ if (!$subscription) {
+ return new Aphront404Response();
+ }
+
+ $can_edit = PhabricatorPolicyFilter::hasCapability(
+ $viewer,
+ $subscription,
+ PhabricatorPolicyCapability::CAN_EDIT);
+
+ $merchant = $subscription->getMerchant();
+ $account = $subscription->getAccount();
+
+ $account_id = $account->getID();
+ $subscription_id = $subscription->getID();
+
+ $title = $subscription->getSubscriptionFullName();
+
+ $header = id(new PHUIHeaderView())
+ ->setHeader($title)
+ ->setHeaderIcon('fa-retweet');
+
+ $edit_uri = $subscription->getEditURI();
+
+ $crumbs = $this->buildApplicationCrumbs()
+ ->addTextCrumb($subscription->getSubscriptionCrumbName())
+ ->setBorder(true);
+
+ $properties = id(new PHUIPropertyListView())
+ ->setUser($viewer);
+
+ $next_invoice = $subscription->getTrigger()->getNextEventPrediction();
+ $properties->addProperty(
+ pht('Next Invoice'),
+ phabricator_datetime($next_invoice, $viewer));
+
+ $autopay = $this->newAutopayView($subscription);
+
+ $details = id(new PHUIObjectBoxView())
+ ->setHeaderText(pht('Subscription Details'))
+ ->setBackground(PHUIObjectBoxView::BLUE_PROPERTY)
+ ->addPropertyList($properties);
+
+ $due_box = $this->buildDueInvoices($subscription);
+ $invoice_box = $this->buildPastInvoices($subscription);
+
+ $timeline = $this->buildTransactionTimeline(
+ $subscription,
+ new PhortuneSubscriptionTransactionQuery());
+ $timeline->setShouldTerminate(true);
+
+ $view = id(new PHUITwoColumnView())
+ ->setHeader($header)
+ ->setFooter(
+ array(
+ $details,
+ $autopay,
+ $due_box,
+ $invoice_box,
+ $timeline,
+ ));
+
+ return $this->newPage()
+ ->setTitle($title)
+ ->setCrumbs($crumbs)
+ ->appendChild($view);
+ }
+
+ private function buildDueInvoices(PhortuneSubscription $subscription) {
+ $viewer = $this->getViewer();
+
+ $invoices = id(new PhortuneCartQuery())
+ ->setViewer($viewer)
+ ->withSubscriptionPHIDs(array($subscription->getPHID()))
+ ->needPurchases(true)
+ ->withInvoices(true)
+ ->execute();
+
+ $phids = array();
+ foreach ($invoices as $invoice) {
+ $phids[] = $invoice->getPHID();
+ $phids[] = $invoice->getMerchantPHID();
+ foreach ($invoice->getPurchases() as $purchase) {
+ $phids[] = $purchase->getPHID();
+ }
+ }
+ $handles = $this->loadViewerHandles($phids);
+
+ $invoice_table = id(new PhortuneOrderTableView())
+ ->setUser($viewer)
+ ->setCarts($invoices)
+ ->setIsInvoices(true)
+ ->setHandles($handles);
+
+ $invoice_header = id(new PHUIHeaderView())
+ ->setHeader(pht('Invoices Due'));
+
+ return id(new PHUIObjectBoxView())
+ ->setHeader($invoice_header)
+ ->setBackground(PHUIObjectBoxView::BLUE_PROPERTY)
+ ->appendChild($invoice_table);
+ }
+
+ private function buildPastInvoices(PhortuneSubscription $subscription) {
+ $viewer = $this->getViewer();
+
+ $invoices = id(new PhortuneCartQuery())
+ ->setViewer($viewer)
+ ->withSubscriptionPHIDs(array($subscription->getPHID()))
+ ->needPurchases(true)
+ ->withStatuses(
+ array(
+ PhortuneCart::STATUS_PURCHASING,
+ PhortuneCart::STATUS_CHARGED,
+ PhortuneCart::STATUS_HOLD,
+ PhortuneCart::STATUS_REVIEW,
+ PhortuneCart::STATUS_PURCHASED,
+ ))
+ ->setLimit(50)
+ ->execute();
+
+ $phids = array();
+ foreach ($invoices as $invoice) {
+ $phids[] = $invoice->getPHID();
+ foreach ($invoice->getPurchases() as $purchase) {
+ $phids[] = $purchase->getPHID();
+ }
+ }
+ $handles = $this->loadViewerHandles($phids);
+
+ $invoice_table = id(new PhortuneOrderTableView())
+ ->setUser($viewer)
+ ->setCarts($invoices)
+ ->setHandles($handles);
+
+ $account = $subscription->getAccount();
+ $merchant = $subscription->getMerchant();
+
+ $account_id = $account->getID();
+ $merchant_id = $merchant->getID();
+ $subscription_id = $subscription->getID();
+
+ $invoices_uri = $this->getApplicationURI(
+ "{$account_id}/subscription/order/{$subscription_id}/");
+
+ $invoice_header = id(new PHUIHeaderView())
+ ->setHeader(pht('Past Invoices'))
+ ->addActionLink(
+ id(new PHUIButtonView())
+ ->setTag('a')
+ ->setIcon('fa-list')
+ ->setHref($invoices_uri)
+ ->setText(pht('View All Invoices')));
+
+ return id(new PHUIObjectBoxView())
+ ->setHeader($invoice_header)
+ ->setBackground(PHUIObjectBoxView::BLUE_PROPERTY)
+ ->appendChild($invoice_table);
+ }
+
+ private function newAutopayView(PhortuneSubscription $subscription) {
+ $viewer = $this->getViewer();
+ $account = $subscription->getAccount();
+
+ $add_method_uri = urisprintf(
+ '/phortune/account/%d/card/new/?subscriptionID=%s',
+ $account->getID(),
+ $subscription->getID());
+ $add_method_uri = $this->getApplicationURI($add_method_uri);
+
+ $can_edit = PhabricatorPolicyFilter::hasCapability(
+ $viewer,
+ $subscription,
+ PhabricatorPolicyCapability::CAN_EDIT);
+
+ $methods = id(new PhortunePaymentMethodQuery())
+ ->setViewer($viewer)
+ ->withAccountPHIDs(array($subscription->getAccountPHID()))
+ ->withMerchantPHIDs(array($subscription->getMerchantPHID()))
+ ->withStatuses(
+ array(
+ PhortunePaymentMethod::STATUS_ACTIVE,
+ ))
+ ->execute();
+ $methods = mpull($methods, null, 'getPHID');
+
+ $autopay_phid = $subscription->getDefaultPaymentMethodPHID();
+ $autopay_method = idx($methods, $autopay_phid);
+
+ $header = id(new PHUIHeaderView())
+ ->setHeader(pht('Autopay'))
+ ->addActionLink(
+ id(new PHUIButtonView())
+ ->setTag('a')
+ ->setIcon('fa-plus')
+ ->setHref($add_method_uri)
+ ->setText(pht('Add Payment Method'))
+ ->setWorkflow(!$can_edit)
+ ->setDisabled(!$can_edit));
+
+ $methods = array_select_keys($methods, array($autopay_phid)) + $methods;
+
+ $rows = array();
+ $rowc = array();
+ foreach ($methods as $method) {
+ $is_autopay = ($autopay_method === $method);
+
+ $remove_uri = urisprintf(
+ '/card/%d/disable/?subscriptionID=%d',
+ $method->getID(),
+ $subscription->getID());
+ $remove_uri = $this->getApplicationURI($remove_uri);
+
+ $autopay_uri = urisprintf(
+ '/account/%d/subscriptions/%d/autopay/%d/',
+ $account->getID(),
+ $subscription->getID(),
+ $method->getID());
+ $autopay_uri = $this->getApplicationURI($autopay_uri);
+
+ $remove_button = id(new PHUIButtonView())
+ ->setTag('a')
+ ->setColor('grey')
+ ->setIcon('fa-times')
+ ->setText(pht('Delete'))
+ ->setHref($remove_uri)
+ ->setWorkflow(true)
+ ->setDisabled(!$can_edit);
+
+ if ($is_autopay) {
+ $autopay_button = id(new PHUIButtonView())
+ ->setColor('red')
+ ->setIcon('fa-times')
+ ->setText(pht('Stop Autopay'));
+ } else {
+ if ($autopay_method) {
+ $make_color = 'grey';
+ } else {
+ $make_color = 'green';
+ }
+
+ $autopay_button = id(new PHUIButtonView())
+ ->setColor($make_color)
+ ->setIcon('fa-retweet')
+ ->setText(pht('Start Autopay'));
+ }
+
+ $autopay_button
+ ->setTag('a')
+ ->setHref($autopay_uri)
+ ->setWorkflow(true)
+ ->setDisabled(!$can_edit);
+
+ $rows[] = array(
+ $method->getID(),
+ phutil_tag(
+ 'a',
+ array(
+ 'href' => $method->getURI(),
+ ),
+ $method->getFullDisplayName()),
+ $method->getDisplayExpires(),
+ $autopay_button,
+ $remove_button,
+ );
+
+ if ($is_autopay) {
+ $rowc[] = 'highlighted';
+ } else {
+ $rowc[] = null;
+ }
+ }
+
+ $method_table = id(new AphrontTableView($rows))
+ ->setHeaders(
+ array(
+ pht('ID'),
+ pht('Payment Method'),
+ pht('Expires'),
+ null,
+ null,
+ ))
+ ->setRowClasses($rowc)
+ ->setColumnClasses(
+ array(
+ null,
+ 'pri wide',
+ null,
+ 'right',
+ null,
+ ));
+
+ if (!$autopay_method) {
+ $method_table->setNotice(
+ array(
+ id(new PHUIIconView())->setIcon('fa-warning yellow'),
+ ' ',
+ pht('Autopay is not currently configured for this subscription.'),
+ ));
+ } else {
+ $method_table->setNotice(
+ array(
+ id(new PHUIIconView())->setIcon('fa-check green'),
+ ' ',
+ pht(
+ 'Autopay is configured using %s.',
+ phutil_tag(
+ 'a',
+ array(
+ 'href' => $autopay_method->getURI(),
+ ),
+ $autopay_method->getFullDisplayName())),
+ ));
+ }
+
+ return id(new PHUIObjectBoxView())
+ ->setHeader($header)
+ ->setBackground(PHUIObjectBoxView::BLUE_PROPERTY)
+ ->setTable($method_table);
+ }
+
+}
diff --git a/src/applications/phortune/controller/paymentmethod/PhortunePaymentMethodDisableController.php b/src/applications/phortune/controller/paymentmethod/PhortunePaymentMethodDisableController.php
--- a/src/applications/phortune/controller/paymentmethod/PhortunePaymentMethodDisableController.php
+++ b/src/applications/phortune/controller/paymentmethod/PhortunePaymentMethodDisableController.php
@@ -24,6 +24,21 @@
return new Aphront400Response();
}
+ $subscription_id = $request->getInt('subscriptionID');
+ if ($subscription_id) {
+ $subscription = id(new PhortuneSubscriptionQuery())
+ ->setViewer($viewer)
+ ->withIDs(array($subscription_id))
+ ->withAccountPHIDs(array($method->getAccountPHID()))
+ ->withMerchantPHIDs(array($method->getMerchantPHID()))
+ ->executeOne();
+ if (!$subscription) {
+ return new Aphront404Response();
+ }
+ } else {
+ $subscription = null;
+ }
+
$account = $method->getAccount();
$account_id = $account->getID();
$account_uri = $account->getPaymentMethodsURI();
@@ -44,18 +59,32 @@
$editor->applyTransactions($method, $xactions);
- return id(new AphrontRedirectResponse())->setURI($account_uri);
+ if ($subscription) {
+ $next_uri = $subscription->getURI();
+ } else {
+ $next_uri = $account_uri;
+ }
+
+ return id(new AphrontRedirectResponse())->setURI($next_uri);
}
+ $method_phid = $method->getPHID();
+ $handles = $viewer->loadHandles(
+ array(
+ $method_phid,
+ ));
+
+ $method_handle = $handles[$method_phid];
+ $method_display = $method_handle->renderLink();
+ $method_display = phutil_tag('strong', array(), $method_display);
+
return $this->newDialog()
->setTitle(pht('Remove Payment Method'))
+ ->addHiddenInput('subscriptionID', $subscription_id)
->appendParagraph(
pht(
- 'Remove the payment method "%s" from your account?',
- phutil_tag(
- 'strong',
- array(),
- $method->getFullDisplayName())))
+ 'Remove the payment method %s from your account?',
+ $method_display))
->appendParagraph(
pht(
'You will no longer be able to make payments using this payment '.
diff --git a/src/applications/phortune/controller/subscription/PhortuneSubscriptionViewController.php b/src/applications/phortune/controller/subscription/PhortuneSubscriptionViewController.php
deleted file mode 100644
--- a/src/applications/phortune/controller/subscription/PhortuneSubscriptionViewController.php
+++ /dev/null
@@ -1,224 +0,0 @@
-<?php
-
-final class PhortuneSubscriptionViewController extends PhortuneController {
-
- public function handleRequest(AphrontRequest $request) {
- $viewer = $this->getViewer();
-
- $authority = $this->loadMerchantAuthority();
-
- $subscription_query = id(new PhortuneSubscriptionQuery())
- ->setViewer($viewer)
- ->withIDs(array($request->getURIData('id')))
- ->needTriggers(true);
-
- if ($authority) {
- $subscription_query->withMerchantPHIDs(array($authority->getPHID()));
- }
-
- $subscription = $subscription_query->executeOne();
- if (!$subscription) {
- return new Aphront404Response();
- }
-
- $can_edit = PhabricatorPolicyFilter::hasCapability(
- $viewer,
- $subscription,
- PhabricatorPolicyCapability::CAN_EDIT);
-
- $merchant = $subscription->getMerchant();
- $account = $subscription->getAccount();
-
- $account_id = $account->getID();
- $subscription_id = $subscription->getID();
-
- $title = $subscription->getSubscriptionFullName();
-
- $header = id(new PHUIHeaderView())
- ->setHeader($title)
- ->setHeaderIcon('fa-calendar-o');
-
- $curtain = $this->newCurtainView($subscription);
- $edit_uri = $subscription->getEditURI();
-
- $curtain->addAction(
- id(new PhabricatorActionView())
- ->setIcon('fa-credit-card')
- ->setName(pht('Manage Autopay'))
- ->setHref($edit_uri)
- ->setDisabled(!$can_edit)
- ->setWorkflow(!$can_edit));
-
- $crumbs = $this->buildApplicationCrumbs();
- if ($authority) {
- $this->addMerchantCrumb($crumbs, $merchant);
- } else {
- $this->addAccountCrumb($crumbs, $account);
- }
- $crumbs->addTextCrumb($subscription->getSubscriptionCrumbName());
- $crumbs->setBorder(true);
-
- $properties = id(new PHUIPropertyListView())
- ->setUser($viewer);
-
- $next_invoice = $subscription->getTrigger()->getNextEventPrediction();
- $properties->addProperty(
- pht('Next Invoice'),
- phabricator_datetime($next_invoice, $viewer));
-
- $default_method = $subscription->getDefaultPaymentMethodPHID();
- if ($default_method) {
- $method = id(new PhortunePaymentMethodQuery())
- ->setViewer($viewer)
- ->withPHIDs(array($default_method))
- ->withStatuses(
- array(
- PhortunePaymentMethod::STATUS_ACTIVE,
- ))
- ->executeOne();
- if ($method) {
- $handles = $this->loadViewerHandles(array($default_method));
- $autopay_method = $handles[$default_method]->renderLink();
- } else {
- $autopay_method = phutil_tag(
- 'em',
- array(),
- pht('<Deleted Payment Method>'));
- }
- } else {
- $autopay_method = phutil_tag(
- 'em',
- array(),
- pht('No Autopay Method Configured'));
- }
-
- $properties->addProperty(
- pht('Autopay With'),
- $autopay_method);
-
- $details = id(new PHUIObjectBoxView())
- ->setHeaderText(pht('Details'))
- ->setBackground(PHUIObjectBoxView::BLUE_PROPERTY)
- ->addPropertyList($properties);
-
- $due_box = $this->buildDueInvoices($subscription, $authority);
- $invoice_box = $this->buildPastInvoices($subscription, $authority);
-
- $view = id(new PHUITwoColumnView())
- ->setHeader($header)
- ->setCurtain($curtain)
- ->setMainColumn(array(
- $details,
- $due_box,
- $invoice_box,
- ));
-
- return $this->newPage()
- ->setTitle($title)
- ->setCrumbs($crumbs)
- ->appendChild($view);
- }
-
- private function buildDueInvoices(
- PhortuneSubscription $subscription,
- $authority) {
- $viewer = $this->getViewer();
-
- $invoices = id(new PhortuneCartQuery())
- ->setViewer($viewer)
- ->withSubscriptionPHIDs(array($subscription->getPHID()))
- ->needPurchases(true)
- ->withInvoices(true)
- ->execute();
-
- $phids = array();
- foreach ($invoices as $invoice) {
- $phids[] = $invoice->getPHID();
- $phids[] = $invoice->getMerchantPHID();
- foreach ($invoice->getPurchases() as $purchase) {
- $phids[] = $purchase->getPHID();
- }
- }
- $handles = $this->loadViewerHandles($phids);
-
- $invoice_table = id(new PhortuneOrderTableView())
- ->setUser($viewer)
- ->setCarts($invoices)
- ->setIsInvoices(true)
- ->setIsMerchantView((bool)$authority)
- ->setHandles($handles);
-
- $invoice_header = id(new PHUIHeaderView())
- ->setHeader(pht('Invoices Due'));
-
- return id(new PHUIObjectBoxView())
- ->setHeader($invoice_header)
- ->setBackground(PHUIObjectBoxView::BLUE_PROPERTY)
- ->appendChild($invoice_table);
- }
-
- private function buildPastInvoices(
- PhortuneSubscription $subscription,
- $authority) {
- $viewer = $this->getViewer();
-
- $invoices = id(new PhortuneCartQuery())
- ->setViewer($viewer)
- ->withSubscriptionPHIDs(array($subscription->getPHID()))
- ->needPurchases(true)
- ->withStatuses(
- array(
- PhortuneCart::STATUS_PURCHASING,
- PhortuneCart::STATUS_CHARGED,
- PhortuneCart::STATUS_HOLD,
- PhortuneCart::STATUS_REVIEW,
- PhortuneCart::STATUS_PURCHASED,
- ))
- ->setLimit(50)
- ->execute();
-
- $phids = array();
- foreach ($invoices as $invoice) {
- $phids[] = $invoice->getPHID();
- foreach ($invoice->getPurchases() as $purchase) {
- $phids[] = $purchase->getPHID();
- }
- }
- $handles = $this->loadViewerHandles($phids);
-
- $invoice_table = id(new PhortuneOrderTableView())
- ->setUser($viewer)
- ->setCarts($invoices)
- ->setHandles($handles);
-
- $account = $subscription->getAccount();
- $merchant = $subscription->getMerchant();
-
- $account_id = $account->getID();
- $merchant_id = $merchant->getID();
- $subscription_id = $subscription->getID();
-
- if ($authority) {
- $invoices_uri = $this->getApplicationURI(
- "merchant/{$merchant_id}/subscription/order/{$subscription_id}/");
- } else {
- $invoices_uri = $this->getApplicationURI(
- "{$account_id}/subscription/order/{$subscription_id}/");
- }
-
- $invoice_header = id(new PHUIHeaderView())
- ->setHeader(pht('Past Invoices'))
- ->addActionLink(
- id(new PHUIButtonView())
- ->setTag('a')
- ->setIcon('fa-list')
- ->setHref($invoices_uri)
- ->setText(pht('View All Invoices')));
-
- return id(new PHUIObjectBoxView())
- ->setHeader($invoice_header)
- ->setBackground(PHUIObjectBoxView::BLUE_PROPERTY)
- ->appendChild($invoice_table);
- }
-
-}
diff --git a/src/applications/phortune/editor/PhortuneSubscriptionEditor.php b/src/applications/phortune/editor/PhortuneSubscriptionEditor.php
new file mode 100644
--- /dev/null
+++ b/src/applications/phortune/editor/PhortuneSubscriptionEditor.php
@@ -0,0 +1,18 @@
+<?php
+
+final class PhortuneSubscriptionEditor
+ extends PhabricatorApplicationTransactionEditor {
+
+ public function getEditorApplicationClass() {
+ return 'PhabricatorPhortuneApplication';
+ }
+
+ public function getEditorObjectsDescription() {
+ return pht('Phortune Subscriptions');
+ }
+
+ public function getCreateObjectTitle($author, $object) {
+ return pht('%s created this subscription.', $author);
+ }
+
+}
diff --git a/src/applications/phortune/phid/PhortunePaymentMethodPHIDType.php b/src/applications/phortune/phid/PhortunePaymentMethodPHIDType.php
--- a/src/applications/phortune/phid/PhortunePaymentMethodPHIDType.php
+++ b/src/applications/phortune/phid/PhortunePaymentMethodPHIDType.php
@@ -32,9 +32,9 @@
foreach ($handles as $phid => $handle) {
$method = $objects[$phid];
- $id = $method->getID();
-
- $handle->setName($method->getFullDisplayName());
+ $handle
+ ->setName($method->getFullDisplayName())
+ ->setURI($method->getURI());
}
}
diff --git a/src/applications/phortune/phid/PhortuneSubscriptionPHIDType.php b/src/applications/phortune/phid/PhortuneSubscriptionPHIDType.php
--- a/src/applications/phortune/phid/PhortuneSubscriptionPHIDType.php
+++ b/src/applications/phortune/phid/PhortuneSubscriptionPHIDType.php
@@ -32,11 +32,9 @@
foreach ($handles as $phid => $handle) {
$subscription = $objects[$phid];
- $id = $subscription->getID();
-
- $handle->setName($subscription->getSubscriptionName());
- $handle->setURI($subscription->getURI());
-
+ $handle
+ ->setName($subscription->getSubscriptionName())
+ ->setURI($subscription->getURI());
}
}
diff --git a/src/applications/phortune/query/PhortuneSubscriptionTransactionQuery.php b/src/applications/phortune/query/PhortuneSubscriptionTransactionQuery.php
new file mode 100644
--- /dev/null
+++ b/src/applications/phortune/query/PhortuneSubscriptionTransactionQuery.php
@@ -0,0 +1,10 @@
+<?php
+
+final class PhortuneSubscriptionTransactionQuery
+ extends PhabricatorApplicationTransactionQuery {
+
+ public function getTemplateApplicationTransaction() {
+ return new PhortuneSubscriptionTransaction();
+ }
+
+}
diff --git a/src/applications/phortune/storage/PhortunePaymentMethod.php b/src/applications/phortune/storage/PhortunePaymentMethod.php
--- a/src/applications/phortune/storage/PhortunePaymentMethod.php
+++ b/src/applications/phortune/storage/PhortunePaymentMethod.php
@@ -180,7 +180,6 @@
}
public function hasAutomaticCapability($capability, PhabricatorUser $viewer) {
-
// See T13366. If you can edit the merchant associated with this payment
// method, you can view the payment method.
if ($capability === PhabricatorPolicyCapability::CAN_VIEW) {
diff --git a/src/applications/phortune/storage/PhortuneSubscription.php b/src/applications/phortune/storage/PhortuneSubscription.php
--- a/src/applications/phortune/storage/PhortuneSubscription.php
+++ b/src/applications/phortune/storage/PhortuneSubscription.php
@@ -3,8 +3,13 @@
/**
* A subscription bills users regularly.
*/
-final class PhortuneSubscription extends PhortuneDAO
- implements PhabricatorPolicyInterface {
+final class PhortuneSubscription
+ extends PhortuneDAO
+ implements
+ PhabricatorPolicyInterface,
+ PhabricatorExtendedPolicyInterface,
+ PhabricatorPolicyCodexInterface,
+ PhabricatorApplicationTransactionInterface {
const STATUS_ACTIVE = 'active';
const STATUS_CANCELLED = 'cancelled';
@@ -55,9 +60,8 @@
) + parent::getConfiguration();
}
- public function generatePHID() {
- return PhabricatorPHID::generateNewPHID(
- PhortuneSubscriptionPHIDType::TYPECONST);
+ public function getPHIDType() {
+ return PhortuneSubscriptionPHIDType::TYPECONST;
}
public static function initializeNewSubscription(
@@ -245,6 +249,16 @@
$purchase);
}
+/* -( PhabricatorApplicationTransactionInterface )------------------------- */
+
+
+ public function getApplicationTransactionEditor() {
+ return new PhortuneSubscriptionEditor();
+ }
+
+ public function getApplicationTransactionTemplate() {
+ return new PhortuneSubscriptionTransaction();
+ }
/* -( PhabricatorPolicyInterface )----------------------------------------- */
@@ -257,26 +271,17 @@
}
public function getPolicy($capability) {
- // NOTE: Both view and edit use the account's edit policy. We punch a hole
- // through this for merchants, below.
- return $this
- ->getAccount()
- ->getPolicy(PhabricatorPolicyCapability::CAN_EDIT);
+ return PhabricatorPolicies::getMostOpenPolicy();
}
public function hasAutomaticCapability($capability, PhabricatorUser $viewer) {
- if ($this->getAccount()->hasAutomaticCapability($capability, $viewer)) {
- return true;
- }
-
- // If the viewer controls the merchant this subscription bills to, they can
- // view the subscription.
- if ($capability == PhabricatorPolicyCapability::CAN_VIEW) {
- $can_admin = PhabricatorPolicyFilter::hasCapability(
- $viewer,
- $this->getMerchant(),
- PhabricatorPolicyCapability::CAN_EDIT);
- if ($can_admin) {
+ // See T13366. If you can edit the merchant associated with this
+ // subscription, you can view the subscription.
+ if ($capability === PhabricatorPolicyCapability::CAN_VIEW) {
+ $any_edit = PhortuneMerchantQuery::canViewersEditMerchants(
+ array($viewer->getPHID()),
+ array($this->getMerchantPHID()));
+ if ($any_edit) {
return true;
}
}
@@ -284,12 +289,31 @@
return false;
}
- public function describeAutomaticCapability($capability) {
+
+/* -( PhabricatorExtendedPolicyInterface )--------------------------------- */
+
+
+ public function getExtendedPolicy($capability, PhabricatorUser $viewer) {
+ if ($this->hasAutomaticCapability($capability, $viewer)) {
+ return array();
+ }
+
+ // See T13366. For blanket view and edit permissions on all subscriptions,
+ // you must be able to edit the associated account.
return array(
- pht('Subscriptions inherit the policies of the associated account.'),
- pht(
- 'The merchant you are subscribed with can review and manage the '.
- 'subscription.'),
+ array(
+ $this->getAccount(),
+ PhabricatorPolicyCapability::CAN_EDIT,
+ ),
);
}
+
+
+/* -( PhabricatorPolicyCodexInterface )------------------------------------ */
+
+
+ public function newPolicyCodex() {
+ return new PhortuneSubscriptionPolicyCodex();
+ }
+
}
diff --git a/src/applications/phortune/storage/PhortuneSubscriptionTransaction.php b/src/applications/phortune/storage/PhortuneSubscriptionTransaction.php
new file mode 100644
--- /dev/null
+++ b/src/applications/phortune/storage/PhortuneSubscriptionTransaction.php
@@ -0,0 +1,18 @@
+<?php
+
+final class PhortuneSubscriptionTransaction
+ extends PhabricatorModularTransaction {
+
+ public function getApplicationName() {
+ return 'phortune';
+ }
+
+ public function getApplicationTransactionType() {
+ return PhortuneSubscriptionPHIDType::TYPECONST;
+ }
+
+ public function getBaseTransactionClass() {
+ return 'PhortuneSubscriptionTransactionType';
+ }
+
+}
diff --git a/src/applications/phortune/xaction/subscription/PhortuneSubscriptionAutopayTransaction.php b/src/applications/phortune/xaction/subscription/PhortuneSubscriptionAutopayTransaction.php
new file mode 100644
--- /dev/null
+++ b/src/applications/phortune/xaction/subscription/PhortuneSubscriptionAutopayTransaction.php
@@ -0,0 +1,41 @@
+<?php
+
+final class PhortuneSubscriptionAutopayTransaction
+ extends PhortuneSubscriptionTransactionType {
+
+ const TRANSACTIONTYPE = 'autopay';
+
+ public function generateOldValue($object) {
+ return $object->getDefaultPaymentMethodPHID();
+ }
+
+ public function applyInternalEffects($object, $value) {
+ $object->setDefaultPaymentMethodPHID($value);
+ }
+
+ public function getTitle() {
+ $old_phid = $this->getOldValue();
+ $new_phid = $this->getNewValue();
+
+ if ($old_phid && $new_phid) {
+ return pht(
+ '%s changed the automatic payment method for this subscription.',
+ $this->renderAuthor());
+ } else if ($new_phid) {
+ return pht(
+ '%s configured an automatic payment method for this subscription.',
+ $this->renderAuthor());
+ } else {
+ return pht(
+ '%s stopped automatic payments for this subscription.',
+ $this->renderAuthor());
+ }
+ }
+
+ public function shouldTryMFA(
+ $object,
+ PhabricatorApplicationTransaction $xaction) {
+ return true;
+ }
+
+}
diff --git a/src/applications/phortune/xaction/subscription/PhortuneSubscriptionTransactionType.php b/src/applications/phortune/xaction/subscription/PhortuneSubscriptionTransactionType.php
new file mode 100644
--- /dev/null
+++ b/src/applications/phortune/xaction/subscription/PhortuneSubscriptionTransactionType.php
@@ -0,0 +1,4 @@
+<?php
+
+abstract class PhortuneSubscriptionTransactionType
+ extends PhabricatorModularTransactionType {}
File Metadata
Details
Attached
Mime Type
text/plain
Expires
Mon, Nov 25, 11:43 PM (15 m, 23 s)
Storage Engine
blob
Storage Format
Encrypted (AES-256-CBC)
Storage Handle
6787398
Default Alt Text
D20721.diff (45 KB)
Attached To
Mode
D20721: Update Phortune subscriptions for modern infrastructure
Attached
Detach File
Event Timeline
Log In to Comment