mirror of
https://we.phorge.it/source/phorge.git
synced 2024-12-28 16:30:59 +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:
parent
6ec1f35870
commit
4c0f15b94b
12 changed files with 322 additions and 23 deletions
16
resources/sql/autopatches/20140721.phortune.3.charge.sql
Normal file
16
resources/sql/autopatches/20140721.phortune.3.charge.sql
Normal 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;
|
|
@ -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',
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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(),
|
||||
|
|
|
@ -49,6 +49,7 @@ final class PhortuneCartQuery
|
|||
$account = idx($accounts, $cart->getAccountPHID());
|
||||
if (!$account) {
|
||||
unset($carts[$key]);
|
||||
continue;
|
||||
}
|
||||
$cart->attachAccount($account);
|
||||
}
|
||||
|
|
93
src/applications/phortune/query/PhortuneChargeQuery.php
Normal file
93
src/applications/phortune/query/PhortuneChargeQuery.php
Normal 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';
|
||||
}
|
||||
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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 )----------------------------------------- */
|
||||
|
||||
|
|
|
@ -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.');
|
||||
}
|
||||
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue