1
0
Fork 0
mirror of https://we.phorge.it/source/phorge.git synced 2024-11-22 14:52:41 +01:00

Phortune Carts and Purchases

Summary:
Ref T2787. Make carts and purchases real objects, with storage, that kind-of work.

Roughly, the idea here is that applications create "purchases" (like "1 large t-shirt") and add them to "carts" (a user can have a lot of different carts at the same time), then hand things off to Phortune to deal with actualy charging a card. Roughly this works like Paypal or other similar systems do, except Phortune is the thing the user gets handed off to.

This doesn't do anything interesting/useful yet.

Also fix some bugs and update some UI.

Test Plan: Added a product to a cart, saw it in cart screen.

Reviewers: btrahan, chad

Reviewed By: chad

Subscribers: epriestley

Maniphest Tasks: T2787

Differential Revision: https://secure.phabricator.com/D10001
This commit is contained in:
epriestley 2014-07-23 10:34:08 -07:00
parent e561a5fe73
commit 6ec1f35870
16 changed files with 1336 additions and 985 deletions

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,11 @@
CREATE TABLE {$NAMESPACE}_phortune.phortune_cart (
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,
metadata LONGTEXT NOT NULL COLLATE utf8_bin,
dateCreated INT UNSIGNED NOT NULL,
dateModified INT UNSIGNED NOT NULL,
UNIQUE KEY `key_phid` (phid),
KEY `key_account` (accountPHID)
) ENGINE=InnoDB, COLLATE utf8_general_ci;

View file

@ -0,0 +1,17 @@
CREATE TABLE {$NAMESPACE}_phortune.phortune_purchase (
id INT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
phid VARCHAR(64) NOT NULL COLLATE utf8_bin,
productPHID 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) COLLATE utf8_bin,
basePriceInCents INT NOT NULL,
quantity INT UNSIGNED NOT NULL,
totalPriceInCents 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)
) ENGINE=InnoDB, COLLATE utf8_general_ci;

View file

@ -2496,6 +2496,7 @@ phutil_register_library_map(array(
'PhortuneAccountViewController' => 'applications/phortune/controller/PhortuneAccountViewController.php',
'PhortuneBalancedPaymentProvider' => 'applications/phortune/provider/PhortuneBalancedPaymentProvider.php',
'PhortuneCart' => 'applications/phortune/storage/PhortuneCart.php',
'PhortuneCartQuery' => 'applications/phortune/query/PhortuneCartQuery.php',
'PhortuneCharge' => 'applications/phortune/storage/PhortuneCharge.php',
'PhortuneConstants' => 'applications/phortune/constants/PhortuneConstants.php',
'PhortuneController' => 'applications/phortune/controller/PhortuneController.php',
@ -2521,12 +2522,14 @@ phutil_register_library_map(array(
'PhortuneProductEditController' => 'applications/phortune/controller/PhortuneProductEditController.php',
'PhortuneProductEditor' => 'applications/phortune/editor/PhortuneProductEditor.php',
'PhortuneProductListController' => 'applications/phortune/controller/PhortuneProductListController.php',
'PhortuneProductPurchaseController' => 'applications/phortune/controller/PhortuneProductPurchaseController.php',
'PhortuneProductQuery' => 'applications/phortune/query/PhortuneProductQuery.php',
'PhortuneProductTransaction' => 'applications/phortune/storage/PhortuneProductTransaction.php',
'PhortuneProductTransactionQuery' => 'applications/phortune/query/PhortuneProductTransactionQuery.php',
'PhortuneProductViewController' => 'applications/phortune/controller/PhortuneProductViewController.php',
'PhortuneProviderController' => 'applications/phortune/controller/PhortuneProviderController.php',
'PhortunePurchase' => 'applications/phortune/storage/PhortunePurchase.php',
'PhortunePurchaseQuery' => 'applications/phortune/query/PhortunePurchaseQuery.php',
'PhortuneStripePaymentProvider' => 'applications/phortune/provider/PhortuneStripePaymentProvider.php',
'PhortuneTestExtraPaymentProvider' => 'applications/phortune/provider/__tests__/PhortuneTestExtraPaymentProvider.php',
'PhortuneTestPaymentProvider' => 'applications/phortune/provider/PhortuneTestPaymentProvider.php',
@ -5374,7 +5377,11 @@ phutil_register_library_map(array(
'PhortuneAccountTransactionQuery' => 'PhabricatorApplicationTransactionQuery',
'PhortuneAccountViewController' => 'PhortuneController',
'PhortuneBalancedPaymentProvider' => 'PhortunePaymentProvider',
'PhortuneCart' => 'PhortuneDAO',
'PhortuneCart' => array(
'PhortuneDAO',
'PhabricatorPolicyInterface',
),
'PhortuneCartQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
'PhortuneCharge' => 'PhortuneDAO',
'PhortuneController' => 'PhabricatorController',
'PhortuneCurrencyTestCase' => 'PhabricatorTestCase',
@ -5402,12 +5409,17 @@ phutil_register_library_map(array(
'PhortuneProductEditController' => 'PhabricatorController',
'PhortuneProductEditor' => 'PhabricatorApplicationTransactionEditor',
'PhortuneProductListController' => 'PhabricatorController',
'PhortuneProductPurchaseController' => 'PhortuneController',
'PhortuneProductQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
'PhortuneProductTransaction' => 'PhabricatorApplicationTransaction',
'PhortuneProductTransactionQuery' => 'PhabricatorApplicationTransactionQuery',
'PhortuneProductViewController' => 'PhortuneController',
'PhortuneProviderController' => 'PhortuneController',
'PhortunePurchase' => 'PhortuneDAO',
'PhortunePurchase' => array(
'PhortuneDAO',
'PhabricatorPolicyInterface',
),
'PhortunePurchaseQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
'PhortuneStripePaymentProvider' => 'PhortunePaymentProvider',
'PhortuneTestExtraPaymentProvider' => 'PhortunePaymentProvider',
'PhortuneTestPaymentProvider' => 'PhortunePaymentProvider',

View file

@ -39,8 +39,9 @@ final class PhabricatorPhortuneApplication extends PhabricatorApplication {
'paymentmethod/' => array(
'edit/' => 'PhortunePaymentMethodEditController',
),
'buy/(?P<id>\d+)/' => 'PhortuneAccountBuyController',
'buy/(?P<productID>\d+)/' => 'PhortuneProductPurchaseController',
),
'cart/(?P<id>\d+)/' => 'PhortuneAccountBuyController',
'account/' => array(
'' => 'PhortuneAccountListController',
'edit/(?:(?P<id>\d+)/)?' => 'PhortuneAccountEditController',

View file

@ -3,11 +3,9 @@
final class PhortuneAccountBuyController
extends PhortuneController {
private $accountID;
private $id;
public function willProcessRequest(array $data) {
$this->accountID = $data['accountID'];
$this->id = $data['id'];
}
@ -15,47 +13,23 @@ final class PhortuneAccountBuyController
$request = $this->getRequest();
$user = $request->getUser();
$account = id(new PhortuneAccountQuery())
->setViewer($user)
->withIDs(array($this->accountID))
->executeOne();
if (!$account) {
return new Aphront404Response();
}
$account_uri = $this->getApplicationURI($account->getID().'/');
$product = id(new PhortuneProductQuery())
$cart = id(new PhortuneCartQuery())
->setViewer($user)
->withIDs(array($this->id))
->needPurchases(true)
->executeOne();
if (!$product) {
if (!$cart) {
return new Aphront404Response();
}
$purchase = new PhortunePurchase();
$purchase->setProductPHID($product->getPHID());
$purchase->setAccountPHID($account->getPHID());
$purchase->setPurchaseName($product->getProductName());
$purchase->setBasePriceInCents($product->getPriceInCents());
$purchase->setQuantity(1);
$purchase->setTotalPriceInCents(
$purchase->getBasePriceInCents() * $purchase->getQuantity());
$purchase->setStatus(PhortunePurchase::STATUS_PENDING);
$cart = new PhortuneCart();
$cart->setAccountPHID($account->getPHID());
$cart->setOwnerPHID($user->getPHID());
$cart->attachPurchases(
array(
$purchase,
));
$account = $cart->getAccount();
$account_uri = $this->getApplicationURI($account->getID().'/');
$rows = array();
$total = 0;
foreach ($cart->getPurchases() as $purchase) {
$rows[] = array(
$purchase->getPurchaseName(),
pht('A Purchase'),
PhortuneCurrency::newFromUSDCents($purchase->getBasePriceInCents())
->formatForDisplay(),
$purchase->getQuantity(),

View file

@ -211,7 +211,8 @@ final class PhortunePaymentMethodEditController
'as a payment method.');
break;
default:
$message = $provider->getCreatePaymentErrorMessage($client_error);
$message = $provider->getCreatePaymentMethodErrorMessage(
$client_error);
if (!$message) {
$message = pht(
"There was an unexpected error ('%s') processing payment ".

View file

@ -142,15 +142,14 @@ final class PhortuneProductEditController extends PhabricatorController {
$is_create ? pht('Create') : pht('Edit'),
$request->getRequestURI());
$header = id(new PHUIHeaderView())
->setHeader(pht('Edit Product'));
$box = id(new PHUIObjectBoxView())
->setHeaderText(pht('Edit Product'))
->appendChild($form);
return $this->buildApplicationPage(
array(
$crumbs,
$header,
$errors,
$form,
$box,
),
array(
'title' => $title,

View file

@ -0,0 +1,70 @@
<?php
final class PhortuneProductPurchaseController
extends PhortuneController {
private $accountID;
private $productID;
public function willProcessRequest(array $data) {
$this->accountID = $data['accountID'];
$this->productID = $data['productID'];
}
public function processRequest() {
$request = $this->getRequest();
$user = $request->getUser();
$account = id(new PhortuneAccountQuery())
->setViewer($user)
->withIDs(array($this->accountID))
->executeOne();
if (!$account) {
return new Aphront404Response();
}
$account_uri = $this->getApplicationURI($account->getID().'/');
$product = id(new PhortuneProductQuery())
->setViewer($user)
->withIDs(array($this->productID))
->executeOne();
if (!$product) {
return new Aphront404Response();
}
if ($request->isFormPost()) {
// TODO: Use ApplicationTransations.
$cart = new PhortuneCart();
$cart->openTransaction();
$cart->setAccountPHID($account->getPHID());
$cart->setAuthorPHID($user->getPHID());
$cart->save();
$purchase = new PhortunePurchase();
$purchase->setProductPHID($product->getPHID());
$purchase->setAccountPHID($account->getPHID());
$purchase->setAuthorPHID($user->getPHID());
$purchase->setCartPHID($cart->getPHID());
$purchase->setBasePriceInCents($product->getPriceInCents());
$purchase->setQuantity(1);
$purchase->setTotalPriceInCents(
$purchase->getBasePriceInCents() * $purchase->getQuantity());
$purchase->setStatus(PhortunePurchase::STATUS_PENDING);
$purchase->save();
$cart->saveTransaction();
$cart_uri = $this->getApplicationURI('/cart/'.$cart->getID().'/');
return id(new AphrontRedirectResponse())->setURI($cart_uri);
}
return $this->newDialog()
->setTitle(pht('Purchase Product'))
->appendParagraph(pht('Really purchase this stuff?'))
->addSubmitButton(pht('Checkout'))
->addCancelButton($account_uri);
}
}

View file

@ -46,7 +46,7 @@ final class PhortuneProductViewController extends PhortuneController {
->setName(pht('Purchase'))
->setHref($cart_uri)
->setIcon('fa-shopping-cart')
->setRenderAsForm(true));
->setWorkflow(true));
$crumbs = $this->buildApplicationCrumbs();
$crumbs->setActionList($actions);

View file

@ -54,7 +54,8 @@ final class PhortuneCurrency {
public static function newFromUSDCents($cents) {
if (!is_int($cents)) {
throw new Exception('USDCents value is not an integer!');
throw new Exception(
pht('USDCents value "%s" is not an integer!', $cents));
}
$obj = new PhortuneCurrency();

View file

@ -110,8 +110,9 @@ final class PhortuneStripePaymentProvider extends PhortunePaymentProvider {
$customer = Stripe_Customer::create($params, $secret_key);
$card = $info->card;
$method
->setBrand($card->type)
->setBrand($card->brand)
->setLastFourDigits($card->last4)
->setExpires($card->exp_year, $card->exp_month)
->setMetadata(

View file

@ -0,0 +1,102 @@
<?php
final class PhortuneCartQuery
extends PhabricatorCursorPagedPolicyAwareQuery {
private $ids;
private $phids;
private $needPurchases;
public function withIDs(array $ids) {
$this->ids = $ids;
return $this;
}
public function withPHIDs(array $phids) {
$this->phids = $phids;
return $this;
}
public function needPurchases($need_purchases) {
$this->needPurchases = $need_purchases;
return $this;
}
protected function loadPage() {
$table = new PhortuneCart();
$conn = $table->establishConnection('r');
$rows = queryfx_all(
$conn,
'SELECT cart.* FROM %T cart %Q %Q %Q',
$table->getTableName(),
$this->buildWhereClause($conn),
$this->buildOrderClause($conn),
$this->buildLimitClause($conn));
return $table->loadAllFromArray($rows);
}
protected function willFilterPage(array $carts) {
$accounts = id(new PhortuneAccountQuery())
->setViewer($this->getViewer())
->withPHIDs(mpull($carts, 'getAccountPHID'))
->execute();
$accounts = mpull($accounts, null, 'getPHID');
foreach ($carts as $key => $cart) {
$account = idx($accounts, $cart->getAccountPHID());
if (!$account) {
unset($carts[$key]);
}
$cart->attachAccount($account);
}
return $carts;
}
protected function didFilterPage(array $carts) {
if ($this->needPurchases) {
$purchases = id(new PhortunePurchaseQuery())
->setViewer($this->getViewer())
->setParentQuery($this)
->withCartPHIDs(mpull($carts, 'getPHID'))
->execute();
$purchases = mgroup($purchases, 'getCartPHID');
foreach ($carts as $cart) {
$cart->attachPurchases(idx($purchases, $cart->getPHID(), array()));
}
}
return $carts;
}
private function buildWhereClause(AphrontDatabaseConnection $conn) {
$where = array();
$where[] = $this->buildPagingClause($conn);
if ($this->ids !== null) {
$where[] = qsprintf(
$conn,
'cart.id IN (%Ld)',
$this->ids);
}
if ($this->phids !== null) {
$where[] = qsprintf(
$conn,
'cart.phid IN (%Ls)',
$this->phids);
}
return $this->formatWhereClause($where);
}
public function getQueryApplicationClass() {
return 'PhabricatorApplicationPhortune';
}
}

View file

@ -0,0 +1,91 @@
<?php
final class PhortunePurchaseQuery
extends PhabricatorCursorPagedPolicyAwareQuery {
private $ids;
private $phids;
private $cartPHIDs;
public function withIDs(array $ids) {
$this->ids = $ids;
return $this;
}
public function withPHIDs(array $phids) {
$this->phids = $phids;
return $this;
}
public function withCartPHIDs(array $cart_phids) {
$this->cartPHIDs = $cart_phids;
return $this;
}
protected function loadPage() {
$table = new PhortunePurchase();
$conn = $table->establishConnection('r');
$rows = queryfx_all(
$conn,
'SELECT purchase.* FROM %T purchase %Q %Q %Q',
$table->getTableName(),
$this->buildWhereClause($conn),
$this->buildOrderClause($conn),
$this->buildLimitClause($conn));
return $table->loadAllFromArray($rows);
}
protected function willFilterPage(array $purchases) {
$carts = id(new PhabricatorObjectQuery())
->setViewer($this->getViewer())
->setParentQuery($this)
->withPHIDs(mpull($purchases, 'getCartPHID'))
->execute();
foreach ($purchases as $key => $purchase) {
$cart = idx($carts, $purchase->getCartPHID());
if (!$cart) {
unset($purchases[$key]);
}
$purchase->attachCart($cart);
}
return $purchases;
}
private function buildWhereClause(AphrontDatabaseConnection $conn) {
$where = array();
$where[] = $this->buildPagingClause($conn);
if ($this->ids !== null) {
$where[] = qsprintf(
$conn,
'purchase.id IN (%Ld)',
$this->ids);
}
if ($this->phids !== null) {
$where[] = qsprintf(
$conn,
'purchase.phid IN (%Ls)',
$this->phids);
}
if ($this->cartPHIDs !== null) {
$where[] = qsprintf(
$conn,
'purchase.cartPHID IN (%Ls)',
$this->cartPHIDs);
}
return $this->formatWhereClause($where);
}
public function getQueryApplicationClass() {
return 'PhabricatorApplicationPhortune';
}
}

View file

@ -1,11 +1,13 @@
<?php
final class PhortuneCart extends PhortuneDAO {
final class PhortuneCart extends PhortuneDAO
implements PhabricatorPolicyInterface {
protected $accountPHID;
protected $ownerPHID;
protected $authorPHID;
protected $metadata;
private $account = self::ATTACHABLE;
private $purchases = self::ATTACHABLE;
public function getConfiguration() {
@ -22,18 +24,50 @@ final class PhortuneCart extends PhortuneDAO {
PhabricatorPHIDConstants::PHID_TYPE_CART);
}
public function getTotalInCents() {
return 123;
}
public function attachPurchases(array $purchases) {
assert_instances_of($purchases, 'PhortunePurchase');
$this->purchases = $purchases;
return $this;
}
public function getTotalInCents() {
return 123;
}
public function getPurchases() {
return $this->assertAttached($this->purchases);
}
public function attachAccount(PhortuneAccount $account) {
$this->account = $account;
return $this;
}
public function getAccount() {
return $this->assertAttached($this->account);
}
/* -( PhabricatorPolicyInterface )----------------------------------------- */
public function getCapabilities() {
return array(
PhabricatorPolicyCapability::CAN_VIEW,
PhabricatorPolicyCapability::CAN_EDIT,
);
}
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('Carts inherit the policies of the associated account.');
}
}

View file

@ -3,7 +3,8 @@
/**
* A purchase represents a user buying something or a subscription to a plan.
*/
final class PhortunePurchase extends PhortuneDAO {
final class PhortunePurchase extends PhortuneDAO
implements PhabricatorPolicyInterface {
const STATUS_PENDING = 'purchase:pending';
const STATUS_PROCESSING = 'purchase:processing';
@ -15,17 +16,15 @@ final class PhortunePurchase extends PhortuneDAO {
protected $productPHID;
protected $accountPHID;
protected $authorPHID;
protected $purchaseName;
protected $purchaseURI;
protected $paymentMethodPHID;
protected $cartPHID;
protected $basePriceInCents;
protected $priceAdjustmentInCents;
protected $finalPriceInCents;
protected $quantity;
protected $totalPriceInCents;
protected $status;
protected $metadata;
private $cart = self::ATTACHABLE;
public function getConfiguration() {
return array(
self::CONFIG_AUX_PHID => true,
@ -40,4 +39,42 @@ final class PhortunePurchase extends PhortuneDAO {
PhabricatorPHIDConstants::PHID_TYPE_PRCH);
}
public function attachCart(PhortuneCart $cart) {
$this->cart = $cart;
return $this;
}
public function getCart() {
return $this->assertAttached($this->cart);
}
protected function didReadData() {
// The payment processing code is strict about types.
$this->basePriceInCents = (int)$this->basePriceInCents;
$this->totalPriceInCents = (int)$this->totalPriceInCents;
}
/* -( PhabricatorPolicyInterface )----------------------------------------- */
public function getCapabilities() {
return array(
PhabricatorPolicyCapability::CAN_VIEW,
PhabricatorPolicyCapability::CAN_EDIT,
);
}
public function getPolicy($capability) {
return $this->getCart()->getPolicy($capability);
}
public function hasAutomaticCapability($capability, PhabricatorUser $viewer) {
return $this->getCart()->hasAutomaticCapability($capability, $viewer);
}
public function describeAutomaticCapability($capability) {
return pht('Purchases have the policies of their cart.');
}
}