1
0
Fork 0
mirror of https://we.phorge.it/source/phorge.git synced 2024-11-25 16:22:43 +01:00

Update Phortune subscriptions for modern infrastructure

Summary:
Depends on D20720. Ref T13366.

  - Use modern policies and policy interfaces.
  - Use new merchant authority cache.
  - Add (some) transactions.
  - Move MFA from pre-upgrade-gate to post-one-shot-check.
  - Simplify the autopay workflow.
  - Use the "reloading arrows" icon for subscriptions more consistently.

Test Plan: As a merchant-authority and account-authority, viewed, edited, and changed autopay for subscriptions.

Subscribers: PHID-OPKG-gm6ozazyms6q6i22gyam

Maniphest Tasks: T13366

Differential Revision: https://secure.phabricator.com/D20721
This commit is contained in:
epriestley 2019-08-16 16:03:23 -07:00
parent 26ec924732
commit a542024b63
19 changed files with 746 additions and 277 deletions

View file

@ -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};

View file

@ -5249,11 +5249,13 @@ phutil_register_library_map(array(
'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 @@ phutil_register_library_map(array(
'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 @@ phutil_register_library_map(array(
'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 @@ phutil_register_library_map(array(
'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',

View file

@ -43,9 +43,7 @@ final class PhabricatorPhortuneApplication extends PhabricatorApplication {
'(?:query/(?P<queryKey>[^/]+)/)?'
=> 'PhortuneSubscriptionListController',
'view/(?P<id>\d+)/'
=> 'PhortuneSubscriptionViewController',
'edit/(?P<id>\d+)/'
=> 'PhortuneSubscriptionEditController',
=> 'PhortuneAccountSubscriptionViewController',
'order/(?P<subscriptionID>\d+)/'
=> 'PhortuneCartListController',
),
@ -73,12 +71,18 @@ final class PhabricatorPhortuneApplication extends PhabricatorApplication {
'(?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 @@ final class PhabricatorPhortuneApplication extends PhabricatorApplication {
'(?:query/(?P<queryKey>[^/]+)/)?'
=> 'PhortuneSubscriptionListController',
'view/(?P<id>\d+)/'
=> 'PhortuneSubscriptionViewController',
=> 'PhortuneAccountSubscriptionViewController',
'order/(?P<subscriptionID>\d+)/'
=> 'PhortuneCartListController',
),

View file

@ -12,6 +12,7 @@ final class PhortunePaymentMethodPolicyCodex
->setCapabilities(
array(
PhabricatorPolicyCapability::CAN_VIEW,
PhabricatorPolicyCapability::CAN_EDIT,
))
->setIsActive(true)
->setDescription(

View file

@ -0,0 +1,36 @@
<?php
final class PhortuneSubscriptionPolicyCodex
extends PhabricatorPolicyCodex {
public function getPolicySpecialRuleDescriptions() {
$object = $this->getObject();
$rules = array();
$rules[] = $this->newRule()
->setCapabilities(
array(
PhabricatorPolicyCapability::CAN_VIEW,
PhabricatorPolicyCapability::CAN_EDIT,
))
->setIsActive(true)
->setDescription(
pht(
'Account members may view and edit subscriptions.'));
$rules[] = $this->newRule()
->setCapabilities(
array(
PhabricatorPolicyCapability::CAN_VIEW,
))
->setIsActive(true)
->setDescription(
pht(
'Merchants you have a relationship with may view associated '.
'subscriptions.'));
return $rules;
}
}

View file

@ -1,6 +1,6 @@
<?php
final class PhortuneAccountPaymentMethodListController
final class PhortuneAccountPaymentMethodController
extends PhortuneAccountProfileController {
protected function shouldRequireAccountEditCapability() {

View file

@ -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;
}
}

View file

@ -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);
}
}

View file

@ -24,6 +24,21 @@ final class PhortunePaymentMethodDisableController
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 @@ final class PhortunePaymentMethodDisableController
$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 '.

View file

@ -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);
}
}

View file

@ -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);
}
}

View file

@ -32,9 +32,9 @@ final class PhortunePaymentMethodPHIDType extends PhabricatorPHIDType {
foreach ($handles as $phid => $handle) {
$method = $objects[$phid];
$id = $method->getID();
$handle->setName($method->getFullDisplayName());
$handle
->setName($method->getFullDisplayName())
->setURI($method->getURI());
}
}

View file

@ -32,11 +32,9 @@ final class PhortuneSubscriptionPHIDType extends PhabricatorPHIDType {
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());
}
}

View file

@ -0,0 +1,10 @@
<?php
final class PhortuneSubscriptionTransactionQuery
extends PhabricatorApplicationTransactionQuery {
public function getTemplateApplicationTransaction() {
return new PhortuneSubscriptionTransaction();
}
}

View file

@ -180,7 +180,6 @@ final class PhortunePaymentMethod
}
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) {

View file

@ -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 @@ final class PhortuneSubscription extends PhortuneDAO
) + parent::getConfiguration();
}
public function generatePHID() {
return PhabricatorPHID::generateNewPHID(
PhortuneSubscriptionPHIDType::TYPECONST);
public function getPHIDType() {
return PhortuneSubscriptionPHIDType::TYPECONST;
}
public static function initializeNewSubscription(
@ -245,6 +249,16 @@ final class PhortuneSubscription extends PhortuneDAO
$purchase);
}
/* -( PhabricatorApplicationTransactionInterface )------------------------- */
public function getApplicationTransactionEditor() {
return new PhortuneSubscriptionEditor();
}
public function getApplicationTransactionTemplate() {
return new PhortuneSubscriptionTransaction();
}
/* -( PhabricatorPolicyInterface )----------------------------------------- */
@ -257,26 +271,17 @@ final class PhortuneSubscription extends PhortuneDAO
}
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 @@ final class PhortuneSubscription extends PhortuneDAO
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();
}
}

View file

@ -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';
}
}

View file

@ -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;
}
}

View file

@ -0,0 +1,4 @@
<?php
abstract class PhortuneSubscriptionTransactionType
extends PhabricatorModularTransactionType {}