From 4c0f15b94b237994ec0d802e2c04631986fe26b9 Mon Sep 17 00:00:00 2001 From: epriestley Date: Wed, 23 Jul 2014 10:36:12 -0700 Subject: [PATCH] Phortune Charges Summary: Ref T2787. Makes charges a real object, allows providers to apply them. We are now (just barely) capable of stealing users' money. Test Plan: {F179584} Reviewers: btrahan, chad Reviewed By: chad Subscribers: epriestley Maniphest Tasks: T2787 Differential Revision: https://secure.phabricator.com/D10002 --- .../20140721.phortune.3.charge.sql | 16 ++++ src/__phutil_library_map__.php | 8 +- .../PhortuneAccountBuyController.php | 71 +++++++++++--- .../PhortuneAccountViewController.php | 52 +++++++++++ .../phortune/currency/PhortuneCurrency.php | 14 ++- .../provider/PhortunePaymentProvider.php | 13 +++ .../PhortuneStripePaymentProvider.php | 3 + .../phortune/query/PhortuneCartQuery.php | 1 + .../phortune/query/PhortuneChargeQuery.php | 93 +++++++++++++++++++ .../phortune/query/PhortunePurchaseQuery.php | 4 +- .../phortune/storage/PhortuneCart.php | 10 ++ .../phortune/storage/PhortuneCharge.php | 60 ++++++++++-- 12 files changed, 322 insertions(+), 23 deletions(-) create mode 100644 resources/sql/autopatches/20140721.phortune.3.charge.sql create mode 100644 src/applications/phortune/query/PhortuneChargeQuery.php diff --git a/resources/sql/autopatches/20140721.phortune.3.charge.sql b/resources/sql/autopatches/20140721.phortune.3.charge.sql new file mode 100644 index 0000000000..b75f05ff1a --- /dev/null +++ b/resources/sql/autopatches/20140721.phortune.3.charge.sql @@ -0,0 +1,16 @@ +CREATE TABLE {$NAMESPACE}_phortune.phortune_charge ( + id INT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY, + phid VARCHAR(64) NOT NULL COLLATE utf8_bin, + accountPHID VARCHAR(64) NOT NULL COLLATE utf8_bin, + authorPHID VARCHAR(64) NOT NULL COLLATE utf8_bin, + cartPHID VARCHAR(64) NOT NULL COLLATE utf8_bin, + paymentMethodPHID VARCHAR(64) NOT NULL COLLATE utf8_bin, + amountInCents INT NOT NULL, + status VARCHAR(32) NOT NULL COLLATE utf8_bin, + metadata LONGTEXT NOT NULL COLLATE utf8_bin, + dateCreated INT UNSIGNED NOT NULL, + dateModified INT UNSIGNED NOT NULL, + UNIQUE KEY `key_phid` (phid), + KEY `key_cart` (cartPHID), + KEY `key_account` (accountPHID) +) ENGINE=InnoDB, COLLATE utf8_general_ci; diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php index 50733ace70..9883926b7a 100644 --- a/src/__phutil_library_map__.php +++ b/src/__phutil_library_map__.php @@ -2498,6 +2498,7 @@ phutil_register_library_map(array( 'PhortuneCart' => 'applications/phortune/storage/PhortuneCart.php', 'PhortuneCartQuery' => 'applications/phortune/query/PhortuneCartQuery.php', 'PhortuneCharge' => 'applications/phortune/storage/PhortuneCharge.php', + 'PhortuneChargeQuery' => 'applications/phortune/query/PhortuneChargeQuery.php', 'PhortuneConstants' => 'applications/phortune/constants/PhortuneConstants.php', 'PhortuneController' => 'applications/phortune/controller/PhortuneController.php', 'PhortuneCreditCardForm' => 'applications/phortune/view/PhortuneCreditCardForm.php', @@ -5382,8 +5383,13 @@ phutil_register_library_map(array( 'PhabricatorPolicyInterface', ), 'PhortuneCartQuery' => 'PhabricatorCursorPagedPolicyAwareQuery', - 'PhortuneCharge' => 'PhortuneDAO', + 'PhortuneCharge' => array( + 'PhortuneDAO', + 'PhabricatorPolicyInterface', + ), + 'PhortuneChargeQuery' => 'PhabricatorCursorPagedPolicyAwareQuery', 'PhortuneController' => 'PhabricatorController', + 'PhortuneCurrency' => 'Phobject', 'PhortuneCurrencyTestCase' => 'PhabricatorTestCase', 'PhortuneDAO' => 'PhabricatorLiskDAO', 'PhortuneErrCode' => 'PhortuneConstants', diff --git a/src/applications/phortune/controller/PhortuneAccountBuyController.php b/src/applications/phortune/controller/PhortuneAccountBuyController.php index 4f23df85c9..e279a84d41 100644 --- a/src/applications/phortune/controller/PhortuneAccountBuyController.php +++ b/src/applications/phortune/controller/PhortuneAccountBuyController.php @@ -11,10 +11,10 @@ final class PhortuneAccountBuyController public function processRequest() { $request = $this->getRequest(); - $user = $request->getUser(); + $viewer = $request->getUser(); $cart = id(new PhortuneCartQuery()) - ->setViewer($user) + ->setViewer($viewer) ->withIDs(array($this->id)) ->needPurchases(true) ->executeOne(); @@ -25,6 +25,56 @@ final class PhortuneAccountBuyController $account = $cart->getAccount(); $account_uri = $this->getApplicationURI($account->getID().'/'); + $methods = id(new PhortunePaymentMethodQuery()) + ->setViewer($viewer) + ->withAccountPHIDs(array($account->getPHID())) + ->withStatus(PhortunePaymentMethodQuery::STATUS_OPEN) + ->execute(); + + $e_method = null; + $errors = array(); + + if ($request->isFormPost()) { + + // Require CAN_EDIT on the cart to actually make purchases. + + PhabricatorPolicyFilter::requireCapability( + $viewer, + $cart, + PhabricatorPolicyCapability::CAN_EDIT); + + $method_id = $request->getInt('paymentMethodID'); + $method = idx($methods, $method_id); + if (!$method) { + $e_method = pht('Required'); + $errors[] = pht('You must choose a payment method.'); + } + + if (!$errors) { + $provider = $method->buildPaymentProvider(); + + $charge = id(new PhortuneCharge()) + ->setAccountPHID($account->getPHID()) + ->setCartPHID($cart->getPHID()) + ->setAuthorPHID($viewer->getPHID()) + ->setPaymentMethodPHID($method->getPHID()) + ->setAmountInCents($cart->getTotalPriceInCents()) + ->setStatus(PhortuneCharge::STATUS_PENDING); + + $charge->openTransaction(); + $charge->save(); + + // TODO: We should be setting some kind of status on the cart here. + $cart->save(); + $charge->saveTransaction(); + + $provider->applyCharge($method, $charge); + + throw new Exception('Executed a charge! Your money is gone forever!'); + } + } + + $rows = array(); $total = 0; foreach ($cart->getPurchases() as $purchase) { @@ -66,20 +116,11 @@ final class PhortuneAccountBuyController $cart_box = id(new PHUIObjectBoxView()) ->setHeaderText(pht('Your Cart')) + ->setFormErrors($errors) ->appendChild($table); $title = pht('Buy Stuff'); - - $methods = id(new PhortunePaymentMethodQuery()) - ->setViewer($user) - ->withAccountPHIDs(array($account->getPHID())) - ->withStatus(PhortunePaymentMethodQuery::STATUS_OPEN) - ->execute(); - - $method_control = id(new AphrontFormRadioButtonControl()) - ->setLabel(pht('Payment Method')); - if (!$methods) { $method_control = id(new AphrontFormStaticControl()) ->setLabel(pht('Payment Method')) @@ -98,11 +139,13 @@ final class PhortuneAccountBuyController } } + $method_control->setError($e_method); + $payment_method_uri = $this->getApplicationURI( $account->getID().'/paymentmethod/edit/'); $form = id(new AphrontFormView()) - ->setUser($user) + ->setUser($viewer) ->appendChild($method_control); $add_providers = PhortunePaymentProvider::getProvidersForAddPaymentMethod(); @@ -137,7 +180,7 @@ final class PhortuneAccountBuyController $one_time_options[] = $provider->renderOneTimePaymentButton( $account, $cart, - $user); + $viewer); } $provider_form = new PHUIFormLayoutView(); diff --git a/src/applications/phortune/controller/PhortuneAccountViewController.php b/src/applications/phortune/controller/PhortuneAccountViewController.php index 8b236cbc3e..1124399d10 100644 --- a/src/applications/phortune/controller/PhortuneAccountViewController.php +++ b/src/applications/phortune/controller/PhortuneAccountViewController.php @@ -56,6 +56,7 @@ final class PhortuneAccountViewController extends PhortuneController { $payment_methods = $this->buildPaymentMethodsSection($account); $purchase_history = $this->buildPurchaseHistorySection($account); + $charge_history = $this->buildChargeHistorySection($account); $account_history = $this->buildAccountHistorySection($account); $object_box = id(new PHUIObjectBoxView()) @@ -68,6 +69,7 @@ final class PhortuneAccountViewController extends PhortuneController { $object_box, $payment_methods, $purchase_history, + $charge_history, $account_history, ), array( @@ -141,6 +143,56 @@ final class PhortuneAccountViewController extends PhortuneController { ->setHeader($header); } + private function buildChargeHistorySection(PhortuneAccount $account) { + $request = $this->getRequest(); + $viewer = $request->getUser(); + + $charges = id(new PhortuneChargeQuery()) + ->setViewer($viewer) + ->withAccountPHIDs(array($account->getPHID())) + ->execute(); + + $rows = array(); + foreach ($charges as $charge) { + $rows[] = array( + $charge->getID(), + $charge->getCartPHID(), + $charge->getPaymentMethodPHID(), + PhortuneCurrency::newFromUSDCents($charge->getAmountInCents()) + ->formatForDisplay(), + $charge->getStatus(), + phabricator_datetime($charge->getDateCreated(), $viewer), + ); + } + + $charge_table = id(new AphrontTableView($rows)) + ->setHeaders( + array( + pht('Charge ID'), + pht('Cart'), + pht('Method'), + pht('Amount'), + pht('Status'), + pht('Created'), + )) + ->setColumnClasses( + array( + '', + '', + '', + 'wide right', + '', + '', + )); + + $header = id(new PHUIHeaderView()) + ->setHeader(pht('Charge History')); + + return id(new PHUIObjectBoxView()) + ->setHeader($header) + ->appendChild($charge_table); + } + private function buildAccountHistorySection(PhortuneAccount $account) { $request = $this->getRequest(); $user = $request->getUser(); diff --git a/src/applications/phortune/currency/PhortuneCurrency.php b/src/applications/phortune/currency/PhortuneCurrency.php index bfa245d80c..0803a2267f 100644 --- a/src/applications/phortune/currency/PhortuneCurrency.php +++ b/src/applications/phortune/currency/PhortuneCurrency.php @@ -1,6 +1,6 @@ getValue(); + } + + return PhortuneCurrency::newFromUSDCents($total); + } + public static function newFromUSDCents($cents) { if (!is_int($cents)) { throw new Exception( diff --git a/src/applications/phortune/provider/PhortunePaymentProvider.php b/src/applications/phortune/provider/PhortunePaymentProvider.php index 9916a1d44d..23d90cab8b 100644 --- a/src/applications/phortune/provider/PhortunePaymentProvider.php +++ b/src/applications/phortune/provider/PhortunePaymentProvider.php @@ -96,6 +96,19 @@ abstract class PhortunePaymentProvider { abstract public function canHandlePaymentMethod( PhortunePaymentMethod $method); + final public function applyCharge( + PhortunePaymentMethod $payment_method, + PhortuneCharge $charge) { + + $charge->setStatus(PhortuneCharge::STATUS_CHARGING); + $charge->save(); + + $this->executeCharge($payment_method, $charge); + + $charge->setStatus(PhortuneCharge::STATUS_CHARGED); + $charge->save(); + } + abstract protected function executeCharge( PhortunePaymentMethod $payment_method, PhortuneCharge $charge); diff --git a/src/applications/phortune/provider/PhortuneStripePaymentProvider.php b/src/applications/phortune/provider/PhortuneStripePaymentProvider.php index 5a294ca27c..7ae2bd5c47 100644 --- a/src/applications/phortune/provider/PhortuneStripePaymentProvider.php +++ b/src/applications/phortune/provider/PhortuneStripePaymentProvider.php @@ -40,6 +40,9 @@ final class PhortuneStripePaymentProvider extends PhortunePaymentProvider { PhortunePaymentMethod $method, PhortuneCharge $charge) { + $root = dirname(phutil_get_library_root('phabricator')); + require_once $root.'/externals/stripe-php/lib/Stripe.php'; + $secret_key = $this->getSecretKey(); $params = array( 'amount' => $charge->getAmountInCents(), diff --git a/src/applications/phortune/query/PhortuneCartQuery.php b/src/applications/phortune/query/PhortuneCartQuery.php index 6b78b8e8ee..5483f5ecc7 100644 --- a/src/applications/phortune/query/PhortuneCartQuery.php +++ b/src/applications/phortune/query/PhortuneCartQuery.php @@ -49,6 +49,7 @@ final class PhortuneCartQuery $account = idx($accounts, $cart->getAccountPHID()); if (!$account) { unset($carts[$key]); + continue; } $cart->attachAccount($account); } diff --git a/src/applications/phortune/query/PhortuneChargeQuery.php b/src/applications/phortune/query/PhortuneChargeQuery.php new file mode 100644 index 0000000000..60f2a03921 --- /dev/null +++ b/src/applications/phortune/query/PhortuneChargeQuery.php @@ -0,0 +1,93 @@ +ids = $ids; + return $this; + } + + public function withPHIDs(array $phids) { + $this->phids = $phids; + return $this; + } + + public function withAccountPHIDs(array $account_phids) { + $this->accountPHIDs = $account_phids; + return $this; + } + + protected function loadPage() { + $table = new PhortuneCharge(); + $conn = $table->establishConnection('r'); + + $rows = queryfx_all( + $conn, + 'SELECT charge.* FROM %T charge %Q %Q %Q', + $table->getTableName(), + $this->buildWhereClause($conn), + $this->buildOrderClause($conn), + $this->buildLimitClause($conn)); + + return $table->loadAllFromArray($rows); + } + + protected function willFilterPage(array $charges) { + $accounts = id(new PhortuneAccountQuery()) + ->setViewer($this->getViewer()) + ->setParentQuery($this) + ->withPHIDs(mpull($charges, 'getAccountPHID')) + ->execute(); + $accounts = mpull($accounts, null, 'getPHID'); + + foreach ($charges as $key => $charge) { + $account = idx($accounts, $charge->getAccountPHID()); + if (!$account) { + unset($charges[$key]); + continue; + } + $charge->attachAccount($account); + } + + return $charges; + } + + private function buildWhereClause(AphrontDatabaseConnection $conn) { + $where = array(); + + $where[] = $this->buildPagingClause($conn); + + if ($this->ids !== null) { + $where[] = qsprintf( + $conn, + 'charge.id IN (%Ld)', + $this->ids); + } + + if ($this->phids !== null) { + $where[] = qsprintf( + $conn, + 'charge.phid IN (%Ls)', + $this->phids); + } + + if ($this->accountPHIDs !== null) { + $where[] = qsprintf( + $conn, + 'charge.accountPHID IN (%Ls)', + $this->accountPHIDs); + } + + return $this->formatWhereClause($where); + } + + public function getQueryApplicationClass() { + return 'PhabricatorApplicationPhortune'; + } + +} diff --git a/src/applications/phortune/query/PhortunePurchaseQuery.php b/src/applications/phortune/query/PhortunePurchaseQuery.php index d5a70e5eee..cce1fa9c81 100644 --- a/src/applications/phortune/query/PhortunePurchaseQuery.php +++ b/src/applications/phortune/query/PhortunePurchaseQuery.php @@ -38,16 +38,18 @@ final class PhortunePurchaseQuery } protected function willFilterPage(array $purchases) { - $carts = id(new PhabricatorObjectQuery()) + $carts = id(new PhortuneCartQuery()) ->setViewer($this->getViewer()) ->setParentQuery($this) ->withPHIDs(mpull($purchases, 'getCartPHID')) ->execute(); + $carts = mpull($carts, null, 'getPHID'); foreach ($purchases as $key => $purchase) { $cart = idx($carts, $purchase->getCartPHID()); if (!$cart) { unset($purchases[$key]); + continue; } $purchase->attachCart($cart); } diff --git a/src/applications/phortune/storage/PhortuneCart.php b/src/applications/phortune/storage/PhortuneCart.php index 3cc69541d7..1237fb23f2 100644 --- a/src/applications/phortune/storage/PhortuneCart.php +++ b/src/applications/phortune/storage/PhortuneCart.php @@ -47,6 +47,16 @@ final class PhortuneCart extends PhortuneDAO return $this->assertAttached($this->account); } + public function getTotalPriceInCents() { + $prices = array(); + foreach ($this->getPurchases() as $purchase) { + $prices[] = PhortuneCurrency::newFromUSDCents( + $purchase->getTotalPriceInCents()); + } + + return PhortuneCurrency::newFromList($prices)->getValue(); + } + /* -( PhabricatorPolicyInterface )----------------------------------------- */ diff --git a/src/applications/phortune/storage/PhortuneCharge.php b/src/applications/phortune/storage/PhortuneCharge.php index 2c7487f53e..64e3833db9 100644 --- a/src/applications/phortune/storage/PhortuneCharge.php +++ b/src/applications/phortune/storage/PhortuneCharge.php @@ -2,12 +2,12 @@ /** * A charge is a charge (or credit) against an account and represents an actual - * transfer of funds. Each charge is normally associated with a product, but a - * product may have multiple charges. For example, a subscription may have - * monthly charges, or a product may have a failed charge followed by a - * successful charge. + * transfer of funds. Each charge is normally associated with a cart, but a + * cart may have multiple charges. For example, a product may have a failed + * charge followed by a successful charge. */ -final class PhortuneCharge extends PhortuneDAO { +final class PhortuneCharge extends PhortuneDAO + implements PhabricatorPolicyInterface { const STATUS_PENDING = 'charge:pending'; const STATUS_AUTHORIZED = 'charge:authorized'; @@ -16,12 +16,15 @@ final class PhortuneCharge extends PhortuneDAO { const STATUS_FAILED = 'charge:failed'; protected $accountPHID; - protected $purchasePHID; + protected $authorPHID; + protected $cartPHID; protected $paymentMethodPHID; protected $amountInCents; protected $status; protected $metadata = array(); + private $account = self::ATTACHABLE; + public function getConfiguration() { return array( self::CONFIG_AUX_PHID => true, @@ -36,4 +39,49 @@ final class PhortuneCharge extends PhortuneDAO { PhabricatorPHIDConstants::PHID_TYPE_CHRG); } + protected function didReadData() { + // The payment processing code is strict about types. + $this->amountInCents = (int)$this->amountInCents; + } + + public function getMetadataValue($key, $default = null) { + return idx($this->metadata, $key, $default); + } + + public function setMetadataValue($key, $value) { + $this->metadata[$key] = $value; + return $this; + } + + public function getAccount() { + return $this->assertAttached($this->account); + } + + public function attachAccount(PhortuneAccount $account) { + $this->account = $account; + return $this; + } + + +/* -( PhabricatorPolicyInterface )----------------------------------------- */ + + + public function getCapabilities() { + return array( + PhabricatorPolicyCapability::CAN_VIEW, + ); + } + + public function getPolicy($capability) { + return $this->getAccount()->getPolicy($capability); + } + + public function hasAutomaticCapability($capability, PhabricatorUser $viewer) { + return $this->getAccount()->hasAutomaticCapability($capability, $viewer); + } + + public function describeAutomaticCapability($capability) { + return pht('Charges inherit the policies of the associated account.'); + } + }