1
0
Fork 0
mirror of https://we.phorge.it/source/phorge.git synced 2024-11-22 14:52:41 +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',
'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',

View file

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

View file

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

View file

@ -1,6 +1,6 @@
<?php
final class PhortuneCurrency {
final class PhortuneCurrency extends Phobject {
private $value;
private $currency;
@ -52,6 +52,18 @@ final class PhortuneCurrency {
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) {
if (!is_int($cents)) {
throw new Exception(

View file

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

View file

@ -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(),

View file

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

View file

@ -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 )----------------------------------------- */

View file

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