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[^/]+)/)?' => 'PhortuneSubscriptionListController', 'view/(?P\d+)/' - => 'PhortuneSubscriptionViewController', - 'edit/(?P\d+)/' - => 'PhortuneSubscriptionEditController', + => 'PhortuneAccountSubscriptionViewController', 'order/(?P\d+)/' => 'PhortuneCartListController', ), @@ -73,12 +71,18 @@ '(?P\d+)/' => array( 'details/' => 'PhortuneAccountDetailsController', 'methods/' => array( - '' => 'PhortuneAccountPaymentMethodListController', + '' => 'PhortuneAccountPaymentMethodController', '(?P\d+)/' => 'PhortuneAccountPaymentMethodViewController', ), 'orders/' => 'PhortuneAccountOrdersController', 'charges/' => 'PhortuneAccountChargesController', - 'subscriptions/' => 'PhortuneAccountSubscriptionController', + 'subscriptions/' => array( + '' => 'PhortuneAccountSubscriptionController', + '(?P\d+)/' => array( + 'autopay/(?P\d+)/' + => 'PhortuneAccountSubscriptionAutopayController', + ), + ), 'managers/' => array( '' => 'PhortuneAccountManagersController', 'add/' => 'PhortuneAccountAddManagerController', @@ -124,7 +128,7 @@ '(?:query/(?P[^/]+)/)?' => 'PhortuneSubscriptionListController', 'view/(?P\d+)/' - => 'PhortuneSubscriptionViewController', + => 'PhortuneAccountSubscriptionViewController', 'order/(?P\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 @@ 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 @@ 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 @@ +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 @@ -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('')); - } - } 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 @@ + $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 @@ +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 @@ +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 @@ +