Page Menu
Home
Phabricator
Search
Configure Global Search
Log In
Files
F13990114
D11596.diff
No One
Temporary
Actions
View File
Edit File
Delete File
View Transforms
Subscribe
Mute Notifications
Award Token
Flag For Later
Size
18 KB
Referenced Files
None
Subscribers
None
D11596.diff
View Options
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
Details
Attached
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)
Attached To
Mode
D11596: Automatically bill subscriptions when a payment method is available
Attached
Detach File
Event Timeline
Log In to Comment