Page MenuHomePhabricator

D11596.diff
No OneTemporary

D11596.diff

diff --git a/resources/sql/autopatches/20150131.phortune.1.defaultpayment.sql b/resources/sql/autopatches/20150131.phortune.1.defaultpayment.sql
new file mode 100644
--- /dev/null
+++ b/resources/sql/autopatches/20150131.phortune.1.defaultpayment.sql
@@ -0,0 +1,2 @@
+ALTER TABLE {$NAMESPACE}_phortune.phortune_subscription
+ ADD defaultPaymentMethodPHID VARBINARY(64);
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
@@ -2812,6 +2812,7 @@
'PhortuneStripePaymentProvider' => 'applications/phortune/provider/PhortuneStripePaymentProvider.php',
'PhortuneSubscription' => 'applications/phortune/storage/PhortuneSubscription.php',
'PhortuneSubscriptionCart' => 'applications/phortune/cart/PhortuneSubscriptionCart.php',
+ 'PhortuneSubscriptionEditController' => 'applications/phortune/controller/PhortuneSubscriptionEditController.php',
'PhortuneSubscriptionImplementation' => 'applications/phortune/subscription/PhortuneSubscriptionImplementation.php',
'PhortuneSubscriptionListController' => 'applications/phortune/controller/PhortuneSubscriptionListController.php',
'PhortuneSubscriptionPHIDType' => 'applications/phortune/phid/PhortuneSubscriptionPHIDType.php',
@@ -6172,6 +6173,7 @@
'PhabricatorPolicyInterface',
),
'PhortuneSubscriptionCart' => 'PhortuneCartImplementation',
+ 'PhortuneSubscriptionEditController' => 'PhortuneController',
'PhortuneSubscriptionListController' => 'PhortuneController',
'PhortuneSubscriptionPHIDType' => 'PhabricatorPHIDType',
'PhortuneSubscriptionProduct' => 'PhortuneProductImplementation',
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
@@ -46,6 +46,8 @@
=> 'PhortuneSubscriptionListController',
'view/(?P<id>\d+)/'
=> 'PhortuneSubscriptionViewController',
+ 'edit/(?P<id>\d+)/'
+ => 'PhortuneSubscriptionEditController',
'order/(?P<subscriptionID>\d+)/'
=> 'PhortuneCartListController',
),
diff --git a/src/applications/phortune/controller/PhortuneSubscriptionEditController.php b/src/applications/phortune/controller/PhortuneSubscriptionEditController.php
new file mode 100644
--- /dev/null
+++ b/src/applications/phortune/controller/PhortuneSubscriptionEditController.php
@@ -0,0 +1,132 @@
+<?php
+
+final class PhortuneSubscriptionEditController extends PhortuneController {
+
+ public function handleRequest(AphrontRequest $request) {
+ $viewer = $this->getViewer();
+
+ $subscription = id(new PhortuneSubscriptionQuery())
+ ->setViewer($viewer)
+ ->withIDs(array($request->getURIData('id')))
+ ->requireCapabilities(
+ array(
+ PhabricatorPolicyCapability::CAN_VIEW,
+ PhabricatorPolicyCapability::CAN_EDIT,
+ ))
+ ->executeOne();
+ if (!$subscription) {
+ return new Aphront404Response();
+ }
+
+ $merchant = $subscription->getMerchant();
+ $account = $subscription->getAccount();
+
+ $title = pht('Subscription: %s', $subscription->getSubscriptionName());
+
+ $header = id(new PHUIHeaderView())
+ ->setHeader($subscription->getSubscriptionName());
+
+ $view_uri = $subscription->getURI();
+
+ $valid_methods = id(new PhortunePaymentMethodQuery())
+ ->setViewer($viewer)
+ ->withAccountPHIDs(array($account->getPHID()))
+ ->requireCapabilities(
+ array(
+ PhabricatorPolicyCapability::CAN_VIEW,
+ PhabricatorPolicyCapability::CAN_EDIT,
+ ))
+ ->execute();
+ $valid_methods = mpull($valid_methods, null, 'getPHID');
+
+ $current_phid = $subscription->getDefaultPaymentMethodPHID();
+
+ $errors = array();
+ if ($request->isFormPost()) {
+
+ $default_method_phid = $request->getStr('defaultPaymentMethodPHID');
+ if (!$default_method_phid) {
+ $default_method_phid = null;
+ $e_method = null;
+ } else if ($default_method_phid == $current_phid) {
+ // If you have an invalid setting already, it's OK to retain it.
+ $e_method = null;
+ } else {
+ if (empty($valid_methods[$default_method_phid])) {
+ $e_method = pht('Invalid');
+ $errors[] = pht('You must select a valid default payment method.');
+ }
+ }
+
+ // TODO: We should use transactions here, and move the validation logic
+ // inside the Editor.
+
+ if (!$errors) {
+ $subscription->setDefaultPaymentMethodPHID($default_method_phid);
+ $subscription->save();
+
+ return id(new AphrontRedirectResponse())
+ ->setURI($view_uri);
+ }
+ }
+
+ // Add the option to disable autopay.
+ $disable_options = array(
+ '' => pht('(Disable Autopay)'),
+ );
+
+ // Don't require the user to make a valid selection if the current method
+ // has become invalid.
+ // TODO: This should probably have a note about why this is bogus.
+ if ($current_phid && empty($valid_methods[$current_phid])) {
+ $handles = $this->loadViewerHandles(array($current_phid));
+ $current_options = array(
+ $current_phid => $handles[$current_phid]->getName(),
+ );
+ } else {
+ $current_options = array();
+ }
+
+ // Add any available options.
+ $valid_options = mpull($valid_methods, 'getFullDisplayName', 'getPHID');
+
+ $options = $disable_options + $current_options + $valid_options;
+
+ $crumbs = $this->buildApplicationCrumbs();
+ $this->addAccountCrumb($crumbs, $account);
+ $crumbs->addTextCrumb(
+ pht('Subscription %d', $subscription->getID()),
+ $view_uri);
+ $crumbs->addTextCrumb(pht('Edit'));
+
+ $form = id(new AphrontFormView())
+ ->setUser($viewer)
+ ->appendChild(
+ id(new AphrontFormSelectControl())
+ ->setName('defaultPaymentMethodPHID')
+ ->setLabel(pht('Autopay With'))
+ ->setValue($current_phid)
+ ->setOptions($options))
+ ->appendChild(
+ id(new AphrontFormSubmitControl())
+ ->setValue(pht('Save Changes'))
+ ->addCancelButton($view_uri));
+
+ $box = id(new PHUIObjectBoxView())
+ ->setUser($viewer)
+ ->setHeaderText(pht('Edit %s', $subscription->getSubscriptionName()))
+ ->setFormErrors($errors)
+ ->appendChild($form);
+
+ return $this->buildApplicationPage(
+ array(
+ $crumbs,
+ $box,
+ ),
+ array(
+ 'title' => $title,
+ ));
+ }
+
+
+}
diff --git a/src/applications/phortune/controller/PhortuneSubscriptionViewController.php b/src/applications/phortune/controller/PhortuneSubscriptionViewController.php
--- a/src/applications/phortune/controller/PhortuneSubscriptionViewController.php
+++ b/src/applications/phortune/controller/PhortuneSubscriptionViewController.php
@@ -14,10 +14,18 @@
return new Aphront404Response();
}
+ $can_edit = PhabricatorPolicyFilter::hasCapability(
+ $viewer,
+ $subscription,
+ PhabricatorPolicyCapability::CAN_EDIT);
+
$is_merchant = (bool)$request->getURIData('merchantID');
$merchant = $subscription->getMerchant();
$account = $subscription->getAccount();
+ $account_id = $account->getID();
+ $subscription_id = $subscription->getID();
+
$title = pht('Subscription: %s', $subscription->getSubscriptionName());
$header = id(new PHUIHeaderView())
@@ -27,6 +35,18 @@
->setUser($viewer)
->setObjectURI($request->getRequestURI());
+ $edit_uri = $this->getApplicationURI(
+ "{$account_id}/subscription/edit/{$subscription_id}/");
+
+ $actions->addAction(
+ id(new PhabricatorActionView())
+ ->setIcon('fa-pencil')
+ ->setName(pht('Edit Subscription'))
+ ->setHref($edit_uri)
+ ->setDisabled(!$can_edit)
+ ->setWorkflow(!$can_edit));
+
+
$crumbs = $this->buildApplicationCrumbs();
if ($is_merchant) {
$this->addMerchantCrumb($crumbs, $merchant);
@@ -44,11 +64,83 @@
pht('Next Invoice'),
phabricator_datetime($next_invoice, $viewer));
+ $default_method = $subscription->getDefaultPaymentMethodPHID();
+ if ($default_method) {
+ $handles = $this->loadViewerHandles(array($default_method));
+ $autopay_method = $handles[$default_method]->renderLink();
+ } else {
+ $autopay_method = phutil_tag(
+ 'em',
+ array(),
+ pht('No Autopay Method Configured'));
+ }
+
+ $properties->addProperty(
+ pht('Autopay With'),
+ $autopay_method);
+
$object_box = id(new PHUIObjectBoxView())
->setHeader($header)
->addPropertyList($properties);
- $carts = id(new PhortuneCartQuery())
+ $due_box = $this->buildDueInvoices($subscription, $is_merchant);
+ $invoice_box = $this->buildPastInvoices($subscription, $is_merchant);
+
+ return $this->buildApplicationPage(
+ array(
+ $crumbs,
+ $object_box,
+ $due_box,
+ $invoice_box,
+ ),
+ array(
+ 'title' => $title,
+ ));
+ }
+
+ private function buildDueInvoices(
+ PhortuneSubscription $subscription,
+ $is_merchant) {
+ $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($is_merchant)
+ ->setHandles($handles);
+
+ $invoice_header = id(new PHUIHeaderView())
+ ->setHeader(pht('Invoices Due'));
+
+ return id(new PHUIObjectBoxView())
+ ->setHeader($invoice_header)
+ ->appendChild($invoice_table);
+ }
+
+ private function buildPastInvoices(
+ PhortuneSubscription $subscription,
+ $is_merchant) {
+ $viewer = $this->getViewer();
+
+ $invoices = id(new PhortuneCartQuery())
->setViewer($viewer)
->withSubscriptionPHIDs(array($subscription->getPHID()))
->needPurchases(true)
@@ -60,12 +152,13 @@
PhortuneCart::STATUS_REVIEW,
PhortuneCart::STATUS_PURCHASED,
))
+ ->setLimit(50)
->execute();
$phids = array();
- foreach ($carts as $cart) {
- $phids[] = $cart->getPHID();
- foreach ($cart->getPurchases() as $purchase) {
+ foreach ($invoices as $invoice) {
+ $phids[] = $invoice->getPHID();
+ foreach ($invoice->getPurchases() as $purchase) {
$phids[] = $purchase->getPHID();
}
}
@@ -73,9 +166,12 @@
$invoice_table = id(new PhortuneOrderTableView())
->setUser($viewer)
- ->setCarts($carts)
+ ->setCarts($invoices)
->setHandles($handles);
+ $account = $subscription->getAccount();
+ $merchant = $subscription->getMerchant();
+
$account_id = $account->getID();
$merchant_id = $merchant->getID();
$subscription_id = $subscription->getID();
@@ -89,7 +185,7 @@
}
$invoice_header = id(new PHUIHeaderView())
- ->setHeader(pht('Recent Invoices'))
+ ->setHeader(pht('Past Invoices'))
->addActionLink(
id(new PHUIButtonView())
->setTag('a')
@@ -99,19 +195,9 @@
->setHref($invoices_uri)
->setText(pht('View All Invoices')));
- $invoice_box = id(new PHUIObjectBoxView())
+ return id(new PHUIObjectBoxView())
->setHeader($invoice_header)
->appendChild($invoice_table);
-
- return $this->buildApplicationPage(
- array(
- $crumbs,
- $object_box,
- $invoice_box,
- ),
- array(
- 'title' => $title,
- ));
}
}
diff --git a/src/applications/phortune/storage/PhortuneCart.php b/src/applications/phortune/storage/PhortuneCart.php
--- a/src/applications/phortune/storage/PhortuneCart.php
+++ b/src/applications/phortune/storage/PhortuneCart.php
@@ -35,7 +35,9 @@
->setAuthorPHID($actor->getPHID())
->setStatus(self::STATUS_BUILDING)
->setAccountPHID($account->getPHID())
- ->setMerchantPHID($merchant->getPHID());
+ ->attachAccount($account)
+ ->setMerchantPHID($merchant->getPHID())
+ ->attachMerchant($merchant);
$cart->account = $account;
$cart->purchases = array();
diff --git a/src/applications/phortune/storage/PhortunePurchase.php b/src/applications/phortune/storage/PhortunePurchase.php
--- a/src/applications/phortune/storage/PhortunePurchase.php
+++ b/src/applications/phortune/storage/PhortunePurchase.php
@@ -31,6 +31,7 @@
return id(new PhortunePurchase())
->setAuthorPHID($actor->getPHID())
->setProductPHID($product->getPHID())
+ ->attachProduct($product)
->setQuantity(1)
->setStatus(self::STATUS_PENDING)
->setBasePriceAsCurrency($product->getPriceAsCurrency());
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
@@ -13,6 +13,7 @@
protected $merchantPHID;
protected $triggerPHID;
protected $authorPHID;
+ protected $defaultPaymentMethodPHID;
protected $subscriptionClassKey;
protected $subscriptionClass;
protected $subscriptionRefKey;
@@ -32,6 +33,7 @@
'metadata' => self::SERIALIZATION_JSON,
),
self::CONFIG_COLUMN_SCHEMA => array(
+ 'defaultPaymentMethodPHID' => 'phid?',
'subscriptionClassKey' => 'bytes12',
'subscriptionClass' => 'text128',
'subscriptionRefKey' => 'bytes12',
@@ -125,7 +127,6 @@
$this->subscriptionRefKey = PhabricatorHash::digestForIndex(
$this->subscriptionRef);
- $trigger = $this->getTrigger();
$is_new = (!$this->getID());
$this->openTransaction();
@@ -153,6 +154,7 @@
),
));
+ $trigger = $this->getTrigger();
$trigger->setPHID($trigger_phid);
$trigger->setAction($trigger_action);
$trigger->save();
diff --git a/src/applications/phortune/view/PhortuneOrderTableView.php b/src/applications/phortune/view/PhortuneOrderTableView.php
--- a/src/applications/phortune/view/PhortuneOrderTableView.php
+++ b/src/applications/phortune/view/PhortuneOrderTableView.php
@@ -6,6 +6,7 @@
private $handles;
private $noDataString;
private $isInvoices;
+ private $isMerchantView;
public function setHandles(array $handles) {
$this->handles = $handles;
@@ -43,12 +44,22 @@
return $this->noDataString;
}
+ public function setIsMerchantView($is_merchant_view) {
+ $this->isMerchantView = $is_merchant_view;
+ return $this;
+ }
+
+ public function getIsMerchantView() {
+ return $this->isMerchantView;
+ }
+
public function render() {
$carts = $this->getCarts();
$handles = $this->getHandles();
$viewer = $this->getUser();
$is_invoices = $this->getIsInvoices();
+ $is_merchant = $this->getIsMerchantView();
$rows = array();
$rowc = array();
@@ -138,7 +149,7 @@
'',
'right',
'right',
- '',
+ 'action',
))
->setColumnVisibility(
array(
@@ -150,7 +161,10 @@
!$is_invoices,
!$is_invoices,
$is_invoices,
- $is_invoices,
+
+ // We show "Pay Now" for due invoices, but not if the viewer is the
+ // merchant, since it doesn't make sense for them to pay.
+ ($is_invoices && !$is_merchant),
));
return $table;
diff --git a/src/applications/phortune/worker/PhortuneSubscriptionWorker.php b/src/applications/phortune/worker/PhortuneSubscriptionWorker.php
--- a/src/applications/phortune/worker/PhortuneSubscriptionWorker.php
+++ b/src/applications/phortune/worker/PhortuneSubscriptionWorker.php
@@ -54,9 +54,69 @@
$cart->setSubscriptionPHID($subscription->getPHID());
$cart->activateCart();
- // TODO: Autocharge this, etc.; this is still mostly faked up.
- echo 'Okay, made a cart here: ';
- echo $cart->getCheckoutURI()."\n\n";
+ try {
+ $issues = $this->chargeSubscription($actor, $subscription, $cart);
+ } catch (Exception $ex) {
+ $issues = array(
+ pht(
+ 'There was a technical error while trying to automatically bill '.
+ 'this subscription: %s',
+ $ex),
+ );
+ }
+
+ if (!$issues) {
+ // We're all done; charging the cart sends a billing email as a side
+ // effect.
+ return;
+ }
+
+ // TODO: Send an email telling the user that we weren't able to autopay
+ // so they need to pay this manually.
+ throw new Exception(implode("\n", $issues));
+ }
+
+
+ private function chargeSubscription(
+ PhabricatorUser $viewer,
+ PhortuneSubscription $subscription,
+ PhortuneCart $cart) {
+
+ $issues = array();
+ if (!$subscription->getDefaultPaymentMethodPHID()) {
+ $issues[] = pht(
+ 'There is no payment method associated with this subscription, so '.
+ 'it could not be billed automatically. Add a default payment method '.
+ 'to enable automatic billing.');
+ return $issues;
+ }
+
+ $method = id(new PhortunePaymentMethodQuery())
+ ->setViewer($viewer)
+ ->withPHIDs(array($subscription->getDefaultPaymentMethodPHID()))
+ ->executeOne();
+ if (!$method) {
+ $issues[] = pht(
+ 'The payment method associated with this subscription is invalid '.
+ 'or out of date, so it could not be automatically billed. Update '.
+ 'the default payment method to enable automatic billing.');
+ return $issues;
+ }
+
+ $provider = $method->buildPaymentProvider();
+ $charge = $cart->willApplyCharge($viewer, $provider, $method);
+
+ try {
+ $provider->applyCharge($method, $charge);
+ } catch (Exception $ex) {
+ $cart->didFailCharge($charge);
+ $issues[] = pht(
+ 'Automatic billing failed: %s',
+ $ex->getMessage());
+ return $issues;
+ }
+
+ $cart->didApplyCharge($charge);
}

File Metadata

Mime Type
text/plain
Expires
Wed, Oct 23, 1:16 AM (2 w, 6 d ago)
Storage Engine
blob
Storage Format
Encrypted (AES-256-CBC)
Storage Handle
6726554
Default Alt Text
D11596.diff (18 KB)

Event Timeline