1
0
Fork 0
mirror of https://we.phorge.it/source/phorge.git synced 2025-02-19 18:28:39 +01:00

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
This commit is contained in:
epriestley 2014-07-23 10:36:12 -07:00
parent 6ec1f35870
commit 4c0f15b94b
12 changed files with 322 additions and 23 deletions

View file

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

View file

@ -2498,6 +2498,7 @@ phutil_register_library_map(array(
'PhortuneCart' => 'applications/phortune/storage/PhortuneCart.php', 'PhortuneCart' => 'applications/phortune/storage/PhortuneCart.php',
'PhortuneCartQuery' => 'applications/phortune/query/PhortuneCartQuery.php', 'PhortuneCartQuery' => 'applications/phortune/query/PhortuneCartQuery.php',
'PhortuneCharge' => 'applications/phortune/storage/PhortuneCharge.php', 'PhortuneCharge' => 'applications/phortune/storage/PhortuneCharge.php',
'PhortuneChargeQuery' => 'applications/phortune/query/PhortuneChargeQuery.php',
'PhortuneConstants' => 'applications/phortune/constants/PhortuneConstants.php', 'PhortuneConstants' => 'applications/phortune/constants/PhortuneConstants.php',
'PhortuneController' => 'applications/phortune/controller/PhortuneController.php', 'PhortuneController' => 'applications/phortune/controller/PhortuneController.php',
'PhortuneCreditCardForm' => 'applications/phortune/view/PhortuneCreditCardForm.php', 'PhortuneCreditCardForm' => 'applications/phortune/view/PhortuneCreditCardForm.php',
@ -5382,8 +5383,13 @@ phutil_register_library_map(array(
'PhabricatorPolicyInterface', 'PhabricatorPolicyInterface',
), ),
'PhortuneCartQuery' => 'PhabricatorCursorPagedPolicyAwareQuery', 'PhortuneCartQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
'PhortuneCharge' => 'PhortuneDAO', 'PhortuneCharge' => array(
'PhortuneDAO',
'PhabricatorPolicyInterface',
),
'PhortuneChargeQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
'PhortuneController' => 'PhabricatorController', 'PhortuneController' => 'PhabricatorController',
'PhortuneCurrency' => 'Phobject',
'PhortuneCurrencyTestCase' => 'PhabricatorTestCase', 'PhortuneCurrencyTestCase' => 'PhabricatorTestCase',
'PhortuneDAO' => 'PhabricatorLiskDAO', 'PhortuneDAO' => 'PhabricatorLiskDAO',
'PhortuneErrCode' => 'PhortuneConstants', 'PhortuneErrCode' => 'PhortuneConstants',

View file

@ -11,10 +11,10 @@ final class PhortuneAccountBuyController
public function processRequest() { public function processRequest() {
$request = $this->getRequest(); $request = $this->getRequest();
$user = $request->getUser(); $viewer = $request->getUser();
$cart = id(new PhortuneCartQuery()) $cart = id(new PhortuneCartQuery())
->setViewer($user) ->setViewer($viewer)
->withIDs(array($this->id)) ->withIDs(array($this->id))
->needPurchases(true) ->needPurchases(true)
->executeOne(); ->executeOne();
@ -25,6 +25,56 @@ final class PhortuneAccountBuyController
$account = $cart->getAccount(); $account = $cart->getAccount();
$account_uri = $this->getApplicationURI($account->getID().'/'); $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(); $rows = array();
$total = 0; $total = 0;
foreach ($cart->getPurchases() as $purchase) { foreach ($cart->getPurchases() as $purchase) {
@ -66,20 +116,11 @@ final class PhortuneAccountBuyController
$cart_box = id(new PHUIObjectBoxView()) $cart_box = id(new PHUIObjectBoxView())
->setHeaderText(pht('Your Cart')) ->setHeaderText(pht('Your Cart'))
->setFormErrors($errors)
->appendChild($table); ->appendChild($table);
$title = pht('Buy Stuff'); $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) { if (!$methods) {
$method_control = id(new AphrontFormStaticControl()) $method_control = id(new AphrontFormStaticControl())
->setLabel(pht('Payment Method')) ->setLabel(pht('Payment Method'))
@ -98,11 +139,13 @@ final class PhortuneAccountBuyController
} }
} }
$method_control->setError($e_method);
$payment_method_uri = $this->getApplicationURI( $payment_method_uri = $this->getApplicationURI(
$account->getID().'/paymentmethod/edit/'); $account->getID().'/paymentmethod/edit/');
$form = id(new AphrontFormView()) $form = id(new AphrontFormView())
->setUser($user) ->setUser($viewer)
->appendChild($method_control); ->appendChild($method_control);
$add_providers = PhortunePaymentProvider::getProvidersForAddPaymentMethod(); $add_providers = PhortunePaymentProvider::getProvidersForAddPaymentMethod();
@ -137,7 +180,7 @@ final class PhortuneAccountBuyController
$one_time_options[] = $provider->renderOneTimePaymentButton( $one_time_options[] = $provider->renderOneTimePaymentButton(
$account, $account,
$cart, $cart,
$user); $viewer);
} }
$provider_form = new PHUIFormLayoutView(); $provider_form = new PHUIFormLayoutView();

View file

@ -56,6 +56,7 @@ final class PhortuneAccountViewController extends PhortuneController {
$payment_methods = $this->buildPaymentMethodsSection($account); $payment_methods = $this->buildPaymentMethodsSection($account);
$purchase_history = $this->buildPurchaseHistorySection($account); $purchase_history = $this->buildPurchaseHistorySection($account);
$charge_history = $this->buildChargeHistorySection($account);
$account_history = $this->buildAccountHistorySection($account); $account_history = $this->buildAccountHistorySection($account);
$object_box = id(new PHUIObjectBoxView()) $object_box = id(new PHUIObjectBoxView())
@ -68,6 +69,7 @@ final class PhortuneAccountViewController extends PhortuneController {
$object_box, $object_box,
$payment_methods, $payment_methods,
$purchase_history, $purchase_history,
$charge_history,
$account_history, $account_history,
), ),
array( array(
@ -141,6 +143,56 @@ final class PhortuneAccountViewController extends PhortuneController {
->setHeader($header); ->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) { private function buildAccountHistorySection(PhortuneAccount $account) {
$request = $this->getRequest(); $request = $this->getRequest();
$user = $request->getUser(); $user = $request->getUser();

View file

@ -1,6 +1,6 @@
<?php <?php
final class PhortuneCurrency { final class PhortuneCurrency extends Phobject {
private $value; private $value;
private $currency; private $currency;
@ -52,6 +52,18 @@ final class PhortuneCurrency {
return $obj; return $obj;
} }
public static function newFromList(array $list) {
assert_instances_of($list, 'PhortuneCurrency');
$total = 0;
foreach ($list as $item) {
// TODO: This should check for integer overflows, etc.
$total += $item->getValue();
}
return PhortuneCurrency::newFromUSDCents($total);
}
public static function newFromUSDCents($cents) { public static function newFromUSDCents($cents) {
if (!is_int($cents)) { if (!is_int($cents)) {
throw new Exception( throw new Exception(

View file

@ -96,6 +96,19 @@ abstract class PhortunePaymentProvider {
abstract public function canHandlePaymentMethod( abstract public function canHandlePaymentMethod(
PhortunePaymentMethod $method); 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( abstract protected function executeCharge(
PhortunePaymentMethod $payment_method, PhortunePaymentMethod $payment_method,
PhortuneCharge $charge); PhortuneCharge $charge);

View file

@ -40,6 +40,9 @@ final class PhortuneStripePaymentProvider extends PhortunePaymentProvider {
PhortunePaymentMethod $method, PhortunePaymentMethod $method,
PhortuneCharge $charge) { PhortuneCharge $charge) {
$root = dirname(phutil_get_library_root('phabricator'));
require_once $root.'/externals/stripe-php/lib/Stripe.php';
$secret_key = $this->getSecretKey(); $secret_key = $this->getSecretKey();
$params = array( $params = array(
'amount' => $charge->getAmountInCents(), 'amount' => $charge->getAmountInCents(),

View file

@ -49,6 +49,7 @@ final class PhortuneCartQuery
$account = idx($accounts, $cart->getAccountPHID()); $account = idx($accounts, $cart->getAccountPHID());
if (!$account) { if (!$account) {
unset($carts[$key]); unset($carts[$key]);
continue;
} }
$cart->attachAccount($account); $cart->attachAccount($account);
} }

View file

@ -0,0 +1,93 @@
<?php
final class PhortuneChargeQuery
extends PhabricatorCursorPagedPolicyAwareQuery {
private $ids;
private $phids;
private $accountPHIDs;
public function withIDs(array $ids) {
$this->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';
}
}

View file

@ -38,16 +38,18 @@ final class PhortunePurchaseQuery
} }
protected function willFilterPage(array $purchases) { protected function willFilterPage(array $purchases) {
$carts = id(new PhabricatorObjectQuery()) $carts = id(new PhortuneCartQuery())
->setViewer($this->getViewer()) ->setViewer($this->getViewer())
->setParentQuery($this) ->setParentQuery($this)
->withPHIDs(mpull($purchases, 'getCartPHID')) ->withPHIDs(mpull($purchases, 'getCartPHID'))
->execute(); ->execute();
$carts = mpull($carts, null, 'getPHID');
foreach ($purchases as $key => $purchase) { foreach ($purchases as $key => $purchase) {
$cart = idx($carts, $purchase->getCartPHID()); $cart = idx($carts, $purchase->getCartPHID());
if (!$cart) { if (!$cart) {
unset($purchases[$key]); unset($purchases[$key]);
continue;
} }
$purchase->attachCart($cart); $purchase->attachCart($cart);
} }

View file

@ -47,6 +47,16 @@ final class PhortuneCart extends PhortuneDAO
return $this->assertAttached($this->account); 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 )----------------------------------------- */ /* -( PhabricatorPolicyInterface )----------------------------------------- */

View file

@ -2,12 +2,12 @@
/** /**
* A charge is a charge (or credit) against an account and represents an actual * 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 * transfer of funds. Each charge is normally associated with a cart, but a
* product may have multiple charges. For example, a subscription may have * cart may have multiple charges. For example, a product may have a failed
* monthly charges, or a product may have a failed charge followed by a * charge followed by a successful charge.
* successful charge.
*/ */
final class PhortuneCharge extends PhortuneDAO { final class PhortuneCharge extends PhortuneDAO
implements PhabricatorPolicyInterface {
const STATUS_PENDING = 'charge:pending'; const STATUS_PENDING = 'charge:pending';
const STATUS_AUTHORIZED = 'charge:authorized'; const STATUS_AUTHORIZED = 'charge:authorized';
@ -16,12 +16,15 @@ final class PhortuneCharge extends PhortuneDAO {
const STATUS_FAILED = 'charge:failed'; const STATUS_FAILED = 'charge:failed';
protected $accountPHID; protected $accountPHID;
protected $purchasePHID; protected $authorPHID;
protected $cartPHID;
protected $paymentMethodPHID; protected $paymentMethodPHID;
protected $amountInCents; protected $amountInCents;
protected $status; protected $status;
protected $metadata = array(); protected $metadata = array();
private $account = self::ATTACHABLE;
public function getConfiguration() { public function getConfiguration() {
return array( return array(
self::CONFIG_AUX_PHID => true, self::CONFIG_AUX_PHID => true,
@ -36,4 +39,49 @@ final class PhortuneCharge extends PhortuneDAO {
PhabricatorPHIDConstants::PHID_TYPE_CHRG); 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.');
}
} }