Page MenuHomePhabricator

D10635.id25543.diff
No OneTemporary

D10635.id25543.diff

diff --git a/src/applications/fund/controller/FundInitiativeBackController.php b/src/applications/fund/controller/FundInitiativeBackController.php
--- a/src/applications/fund/controller/FundInitiativeBackController.php
+++ b/src/applications/fund/controller/FundInitiativeBackController.php
@@ -47,6 +47,7 @@
$currency = PhortuneCurrency::newFromUserInput(
$viewer,
$v_amount);
+ $currency->assertInRange('1.00 USD', null);
} catch (Exception $ex) {
$errors[] = $ex->getMessage();
$e_amount = pht('Invalid');
@@ -72,7 +73,10 @@
$cart = $account->newCart($viewer);
$purchase = $cart->newPurchase($viewer, $product);
- $purchase->setBasePriceAsCurrency($currency)->save();
+ $purchase
+ ->setBasePriceAsCurrency($currency)
+ ->setMetadataValue('backerPHID', $backer->getPHID())
+ ->save();
$xactions = array();
@@ -86,6 +90,8 @@
$editor->applyTransactions($backer, $xactions);
+ $cart->activateCart();
+
return id(new AphrontRedirectResponse())
->setURI($cart->getCheckoutURI());
}
diff --git a/src/applications/fund/editor/FundInitiativeEditor.php b/src/applications/fund/editor/FundInitiativeEditor.php
--- a/src/applications/fund/editor/FundInitiativeEditor.php
+++ b/src/applications/fund/editor/FundInitiativeEditor.php
@@ -17,6 +17,7 @@
$types[] = FundInitiativeTransaction::TYPE_NAME;
$types[] = FundInitiativeTransaction::TYPE_DESCRIPTION;
$types[] = FundInitiativeTransaction::TYPE_STATUS;
+ $types[] = FundInitiativeTransaction::TYPE_BACKER;
$types[] = PhabricatorTransactions::TYPE_VIEW_POLICY;
$types[] = PhabricatorTransactions::TYPE_EDIT_POLICY;
@@ -33,6 +34,8 @@
return $object->getDescription();
case FundInitiativeTransaction::TYPE_STATUS:
return $object->getStatus();
+ case FundInitiativeTransaction::TYPE_BACKER:
+ return null;
}
return parent::getCustomTransactionOldValue($object, $xaction);
@@ -46,6 +49,7 @@
case FundInitiativeTransaction::TYPE_NAME:
case FundInitiativeTransaction::TYPE_DESCRIPTION:
case FundInitiativeTransaction::TYPE_STATUS:
+ case FundInitiativeTransaction::TYPE_BACKER:
return $xaction->getNewValue();
}
@@ -66,6 +70,9 @@
case FundInitiativeTransaction::TYPE_STATUS:
$object->setStatus($xaction->getNewValue());
return;
+ case FundInitiativeTransaction::TYPE_BACKER:
+ // TODO: Calculate total funding / backers / etc.
+ return;
case PhabricatorTransactions::TYPE_SUBSCRIBERS:
case PhabricatorTransactions::TYPE_EDGE:
return;
@@ -82,6 +89,9 @@
case FundInitiativeTransaction::TYPE_NAME:
case FundInitiativeTransaction::TYPE_DESCRIPTION:
case FundInitiativeTransaction::TYPE_STATUS:
+ case FundInitiativeTransaction::TYPE_BACKER:
+ // TODO: Maybe we should apply the backer transaction from here?
+ return;
case PhabricatorTransactions::TYPE_SUBSCRIBERS:
case PhabricatorTransactions::TYPE_EDGE:
return;
diff --git a/src/applications/fund/phortune/FundBackerProduct.php b/src/applications/fund/phortune/FundBackerProduct.php
--- a/src/applications/fund/phortune/FundBackerProduct.php
+++ b/src/applications/fund/phortune/FundBackerProduct.php
@@ -4,13 +4,27 @@
private $initiativePHID;
private $initiative;
+ private $viewer;
+
+ public function setViewer(PhabricatorUser $viewer) {
+ $this->viewer = $viewer;
+ return $this;
+ }
+
+ public function getViewer() {
+ return $this->viewer;
+ }
public function getRef() {
return $this->getInitiativePHID();
}
public function getName(PhortuneProduct $product) {
- return pht('Back Initiative %s', $this->initiativePHID);
+ $initiative = $this->getInitiative();
+ return pht(
+ 'Back Initiative %s %s',
+ $initiative->getMonogram(),
+ $initiative->getName());
}
public function getPriceAsCurrency(PhortuneProduct $product) {
@@ -48,6 +62,7 @@
$objects = array();
foreach ($refs as $ref) {
$object = id(new FundBackerProduct())
+ ->setViewer($viewer)
->setInitiativePHID($ref);
$initiative = idx($initiatives, $ref);
@@ -61,4 +76,43 @@
return $objects;
}
+ public function didPurchaseProduct(
+ PhortuneProduct $product,
+ PhortunePurchase $purchase) {
+ $viewer = $this->getViewer();
+
+ $backer = id(new FundBackerQuery())
+ ->setViewer($viewer)
+ ->withPHIDs(array($purchase->getMetadataValue('backerPHID')))
+ ->executeOne();
+ if (!$backer) {
+ throw new Exception(pht('Unable to load FundBacker!'));
+ }
+
+ $xactions = array();
+ $xactions[] = id(new FundBackerTransaction())
+ ->setTransactionType(FundBackerTransaction::TYPE_STATUS)
+ ->setNewValue(FundBacker::STATUS_PURCHASED);
+
+ $editor = id(new FundBackerEditor())
+ ->setActor($viewer)
+ ->setContentSource($this->getContentSource());
+
+ $editor->applyTransactions($backer, $xactions);
+
+
+ $xactions = array();
+ $xactions[] = id(new FundInitiativeTransaction())
+ ->setTransactionType(FundInitiativeTransaction::TYPE_BACKER)
+ ->setNewValue($backer->getPHID());
+
+ $editor = id(new FundInitiativeEditor())
+ ->setActor($viewer)
+ ->setContentSource($this->getContentSource());
+
+ $editor->applyTransactions($this->getInitiative(), $xactions);
+
+ return;
+ }
+
}
diff --git a/src/applications/fund/query/FundBackerQuery.php b/src/applications/fund/query/FundBackerQuery.php
--- a/src/applications/fund/query/FundBackerQuery.php
+++ b/src/applications/fund/query/FundBackerQuery.php
@@ -5,6 +5,7 @@
private $ids;
private $phids;
+ private $statuses;
private $initiativePHIDs;
private $backerPHIDs;
@@ -19,6 +20,11 @@
return $this;
}
+ public function withStatuses(array $statuses) {
+ $this->statuses = $statuses;
+ return $this;
+ }
+
public function withInitiativePHIDs(array $phids) {
$this->initiativePHIDs = $phids;
return $this;
@@ -95,6 +101,13 @@
$this->backerPHIDs);
}
+ if ($this->statuses !== null) {
+ $where[] = qsprintf(
+ $conn_r,
+ 'status IN (%Ls)',
+ $this->statuses);
+ }
+
return $this->formatWhereClause($where);
}
diff --git a/src/applications/fund/query/FundBackerSearchEngine.php b/src/applications/fund/query/FundBackerSearchEngine.php
--- a/src/applications/fund/query/FundBackerSearchEngine.php
+++ b/src/applications/fund/query/FundBackerSearchEngine.php
@@ -35,6 +35,8 @@
public function buildQueryFromSavedQuery(PhabricatorSavedQuery $saved) {
$query = id(new FundBackerQuery());
+ $query->withStatuses(array(FundBacker::STATUS_PURCHASED));
+
if ($this->getInitiative()) {
$query->withInitiativePHIDs(
array(
@@ -128,7 +130,7 @@
foreach ($backers as $backer) {
$backer_handle = $handles[$backer->getBackerPHID()];
- $currency = $backer->getAmount();
+ $currency = $backer->getAmountAsCurrency();
$header = pht(
'%s for %s',
diff --git a/src/applications/fund/storage/FundBacker.php b/src/applications/fund/storage/FundBacker.php
--- a/src/applications/fund/storage/FundBacker.php
+++ b/src/applications/fund/storage/FundBacker.php
@@ -15,6 +15,7 @@
const STATUS_NEW = 'new';
const STATUS_IN_CART = 'in-cart';
+ const STATUS_PURCHASED = 'purchased';
public static function initializeNewBacker(PhabricatorUser $actor) {
return id(new FundBacker())
diff --git a/src/applications/fund/storage/FundInitiativeTransaction.php b/src/applications/fund/storage/FundInitiativeTransaction.php
--- a/src/applications/fund/storage/FundInitiativeTransaction.php
+++ b/src/applications/fund/storage/FundInitiativeTransaction.php
@@ -6,6 +6,7 @@
const TYPE_NAME = 'fund:name';
const TYPE_DESCRIPTION = 'fund:description';
const TYPE_STATUS = 'fund:status';
+ const TYPE_BACKER = 'fund:backer';
public function getApplicationName() {
return 'fund';
@@ -57,6 +58,10 @@
$this->renderHandleLink($author_phid));
}
break;
+ case FundInitiativeTransaction::TYPE_BACKER:
+ return pht(
+ '%s backed this initiative.',
+ $this->renderHandleLink($author_phid));
}
return parent::getTitle();
@@ -104,6 +109,11 @@
$this->renderHandleLink($object_phid));
}
break;
+ case FundInitiativeTransaction::TYPE_BACKER:
+ return pht(
+ '%s backed %s.',
+ $this->renderHandleLink($author_phid),
+ $this->renderHandleLink($object_phid));
}
return parent::getTitleForFeed($story);
diff --git a/src/applications/metamta/contentsource/PhabricatorContentSource.php b/src/applications/metamta/contentsource/PhabricatorContentSource.php
--- a/src/applications/metamta/contentsource/PhabricatorContentSource.php
+++ b/src/applications/metamta/contentsource/PhabricatorContentSource.php
@@ -14,6 +14,7 @@
const SOURCE_LEGACY = 'legacy';
const SOURCE_DAEMON = 'daemon';
const SOURCE_LIPSUM = 'lipsum';
+ const SOURCE_PHORTUNE = 'phortune';
private $source;
private $params = array();
@@ -77,6 +78,7 @@
self::SOURCE_DAEMON => pht('Daemons'),
self::SOURCE_LIPSUM => pht('Lipsum'),
self::SOURCE_UNKNOWN => pht('Old World'),
+ self::SOURCE_PHORTUNE => pht('Phortune'),
);
}
diff --git a/src/applications/phortune/controller/PhortuneCartCheckoutController.php b/src/applications/phortune/controller/PhortuneCartCheckoutController.php
--- a/src/applications/phortune/controller/PhortuneCartCheckoutController.php
+++ b/src/applications/phortune/controller/PhortuneCartCheckoutController.php
@@ -22,6 +22,42 @@
return new Aphront404Response();
}
+ $cancel_uri = $cart->getCancelURI();
+
+ switch ($cart->getStatus()) {
+ case PhortuneCart::STATUS_BUILDING:
+ return $this->newDialog()
+ ->setTitle(pht('Incomplete Cart'))
+ ->appendParagraph(
+ pht(
+ 'The application that created this cart did not finish putting '.
+ 'products in it. You can not checkout with an incomplete '.
+ 'cart.'))
+ ->addCancelButton($cancel_uri);
+ case PhortuneCart::STATUS_READY:
+ // This is the expected, normal state for a cart that's ready for
+ // checkout.
+ break;
+ case PhortuneCart::STATUS_CHARGED:
+ // TODO: This is really bad (we took your money and at least partially
+ // failed to fulfill your order) and should have better steps forward.
+
+ return $this->newDialog()
+ ->setTitle(pht('Purchase Failed'))
+ ->appendParagraph(
+ pht(
+ 'This cart was charged but the purchase could not be '.
+ 'completed.'))
+ ->addCancelButton($cancel_uri);
+ case PhortuneCart::STATUS_PURCHASED:
+ return id(new AphrontRedirectResponse())->setURI($cart->getDetailURI());
+ default:
+ throw new Exception(
+ pht(
+ 'Unknown cart status "%s"!',
+ $cart->getStatus()));
+ }
+
$account = $cart->getAccount();
$account_uri = $this->getApplicationURI($account->getID().'/');
@@ -71,12 +107,10 @@
$provider->applyCharge($method, $charge);
- $cart->setStatus(PhortuneCart::STATUS_PURCHASED);
- $cart->save();
-
- $view_uri = $this->getApplicationURI('cart/'.$cart->getID().'/');
+ $cart->didApplyCharge($charge);
- return id(new AphrontRedirectResponse())->setURI($view_uri);
+ $done_uri = $cart->getDoneURI();
+ return id(new AphrontRedirectResponse())->setURI($done_uri);
}
}
diff --git a/src/applications/phortune/currency/PhortuneCurrency.php b/src/applications/phortune/currency/PhortuneCurrency.php
--- a/src/applications/phortune/currency/PhortuneCurrency.php
+++ b/src/applications/phortune/currency/PhortuneCurrency.php
@@ -125,6 +125,54 @@
throw new Exception("Invalid currency format ('{$string}').");
}
+ /**
+ * Assert that a currency value lies within a range.
+ *
+ * Throws if the value is not between the minimum and maximum, inclusive.
+ *
+ * In particular, currency values can be negative (to represent a debt or
+ * credit), so checking against zero may be useful to make sure a value
+ * has the expected sign.
+ *
+ * @param string|null Currency string, or null to skip check.
+ * @param string|null Currency string, or null to skip check.
+ * @return this
+ */
+ public function assertInRange($minimum, $maximum) {
+ if ($minimum !== null && $maximum !== null) {
+ $min = PhortuneCurrency::newFromString($minimum);
+ $max = PhortuneCurrency::newFromString($maximum);
+ if ($min->value > $max->value) {
+ throw new Exception(
+ pht(
+ 'Range (%s - %s) is not valid!',
+ $min->formatForDisplay(),
+ $max->formatForDisplay()));
+ }
+ }
+
+ if ($minimum !== null) {
+ $min = PhortuneCurrency::newFromString($minimum);
+ if ($min->value > $this->value) {
+ throw new Exception(
+ pht(
+ 'Minimum allowed amount is %s.',
+ $min->formatForDisplay()));
+ }
+ }
+
+ if ($maximum !== null) {
+ $max = PhortuneCurrency::newFromString($maximum);
+ if ($max->value < $this->value) {
+ throw new Exception(
+ pht(
+ 'Maximum allowed amount is %s.',
+ $max->formatForDisplay()));
+ }
+ }
+
+ return $this;
+ }
}
diff --git a/src/applications/phortune/currency/__tests__/PhortuneCurrencyTestCase.php b/src/applications/phortune/currency/__tests__/PhortuneCurrencyTestCase.php
--- a/src/applications/phortune/currency/__tests__/PhortuneCurrencyTestCase.php
+++ b/src/applications/phortune/currency/__tests__/PhortuneCurrencyTestCase.php
@@ -86,4 +86,60 @@
}
}
+ public function testCurrencyRanges() {
+ $value = PhortuneCurrency::newFromString('3.00 USD');
+
+ $value->assertInRange('2.00 USD', '4.00 USD');
+ $value->assertInRange('2.00 USD', null);
+ $value->assertInRange(null, '4.00 USD');
+ $value->assertInRange(null, null);
+
+ $caught = null;
+ try {
+ $value->assertInRange('4.00 USD', null);
+ } catch (Exception $ex) {
+ $caught = $ex;
+ }
+ $this->assertTrue($caught instanceof Exception);
+
+ $caught = null;
+ try {
+ $value->assertInRange(null, '2.00 USD');
+ } catch (Exception $ex) {
+ $caught = $ex;
+ }
+ $this->assertTrue($caught instanceof Exception);
+
+ $caught = null;
+ try {
+ // Minimum and maximum are reversed here.
+ $value->assertInRange('4.00 USD', '2.00 USD');
+ } catch (Exception $ex) {
+ $caught = $ex;
+ }
+ $this->assertTrue($caught instanceof Exception);
+
+ $credit = PhortuneCurrency::newFromString('-3.00 USD');
+ $credit->assertInRange('-4.00 USD', '-2.00 USD');
+ $credit->assertInRange('-4.00 USD', null);
+ $credit->assertInRange(null, '-2.00 USD');
+ $credit->assertInRange(null, null);
+
+ $caught = null;
+ try {
+ $credit->assertInRange('-2.00 USD', null);
+ } catch (Exception $ex) {
+ $caught = $ex;
+ }
+ $this->assertTrue($caught instanceof Exception);
+
+ $caught = null;
+ try {
+ $credit->assertInRange(null, '-4.00 USD');
+ } catch (Exception $ex) {
+ $caught = $ex;
+ }
+ $this->assertTrue($caught instanceof Exception);
+ }
+
}
diff --git a/src/applications/phortune/product/PhortuneProductImplementation.php b/src/applications/phortune/product/PhortuneProductImplementation.php
--- a/src/applications/phortune/product/PhortuneProductImplementation.php
+++ b/src/applications/phortune/product/PhortuneProductImplementation.php
@@ -10,4 +10,22 @@
abstract public function getName(PhortuneProduct $product);
abstract public function getPriceAsCurrency(PhortuneProduct $product);
+ protected function getContentSource() {
+ return PhabricatorContentSource::newForSource(
+ PhabricatorContentSource::SOURCE_PHORTUNE,
+ array());
+ }
+
+ public function getPurchaseName(
+ PhortuneProduct $product,
+ PhortunePurchase $purchase) {
+ return $this->getName($product);
+ }
+
+ public function didPurchaseProduct(
+ PhortuneProduct $product,
+ PhortunePurchase $purchase) {
+ return;
+ }
+
}
diff --git a/src/applications/phortune/query/PhortunePurchaseQuery.php b/src/applications/phortune/query/PhortunePurchaseQuery.php
--- a/src/applications/phortune/query/PhortunePurchaseQuery.php
+++ b/src/applications/phortune/query/PhortunePurchaseQuery.php
@@ -54,6 +54,23 @@
$purchase->attachCart($cart);
}
+ $products = id(new PhortuneProductQuery())
+ ->setViewer($this->getViewer())
+ ->setParentQuery($this)
+ ->withPHIDs(mpull($purchases, 'getProductPHID'))
+ ->execute();
+ $products = mpull($products, null, 'getPHID');
+
+ foreach ($purchases as $key => $purchase) {
+ $product = idx($products, $purchase->getProductPHID());
+ if (!$product) {
+ unset($purchases[$key]);
+ continue;
+ }
+ $purchase->attachProduct($product);
+ }
+
+
return $purchases;
}
diff --git a/src/applications/phortune/storage/PhortuneCart.php b/src/applications/phortune/storage/PhortuneCart.php
--- a/src/applications/phortune/storage/PhortuneCart.php
+++ b/src/applications/phortune/storage/PhortuneCart.php
@@ -3,8 +3,10 @@
final class PhortuneCart extends PhortuneDAO
implements PhabricatorPolicyInterface {
+ const STATUS_BUILDING = 'cart:building';
const STATUS_READY = 'cart:ready';
const STATUS_PURCHASING = 'cart:purchasing';
+ const STATUS_CHARGED = 'cart:charged';
const STATUS_PURCHASED = 'cart:purchased';
protected $accountPHID;
@@ -20,7 +22,7 @@
PhortuneAccount $account) {
$cart = id(new PhortuneCart())
->setAuthorPHID($actor->getPHID())
- ->setStatus(self::STATUS_READY)
+ ->setStatus(self::STATUS_BUILDING)
->setAccountPHID($account->getPHID());
$cart->account = $account;
@@ -43,6 +45,47 @@
return $purchase;
}
+ public function activateCart() {
+ $this->setStatus(self::STATUS_READY)->save();
+ return $this;
+ }
+
+ public function didApplyCharge(PhortuneCharge $charge) {
+ if ($this->getStatus() !== self::STATUS_PURCHASING) {
+ throw new Exception(
+ pht(
+ 'Cart has wrong status ("%s") to call didApplyCharge(), expected '.
+ '"%s".',
+ $this->getStatus(),
+ self::STATUS_PURCHASING));
+ }
+
+ $this->setStatus(self::STATUS_CHARGED)->save();
+
+ foreach ($this->purchases as $purchase) {
+ $purchase->getProduct()->didPurchaseProduct($purchase);
+ }
+
+ $this->setStatus(self::STATUS_PURCHASED)->save();
+
+ return $this;
+ }
+
+
+ public function getDoneURI() {
+ // TODO: Implement properly.
+ return '/phortune/cart/'.$this->getID().'/';
+ }
+
+ public function getCancelURI() {
+ // TODO: Implement properly.
+ return '/';
+ }
+
+ public function getDetailURI() {
+ return '/phortune/cart/'.$this->getID().'/';
+ }
+
public function getCheckoutURI() {
return '/phortune/cart/'.$this->getID().'/checkout/';
}
diff --git a/src/applications/phortune/storage/PhortuneProduct.php b/src/applications/phortune/storage/PhortuneProduct.php
--- a/src/applications/phortune/storage/PhortuneProduct.php
+++ b/src/applications/phortune/storage/PhortuneProduct.php
@@ -70,6 +70,14 @@
return $this->getImplementation()->getName($this);
}
+ public function getPurchaseName(PhortunePurchase $purchase) {
+ return $this->getImplementation()->getPurchaseName($this, $purchase);
+ }
+
+ public function didPurchaseProduct(PhortunePurchase $purchase) {
+ return $this->getImplementation()->didPurchaseProduct($this, $purchase);
+ }
+
/* -( PhabricatorPolicyInterface )----------------------------------------- */
diff --git a/src/applications/phortune/storage/PhortunePurchase.php b/src/applications/phortune/storage/PhortunePurchase.php
--- a/src/applications/phortune/storage/PhortunePurchase.php
+++ b/src/applications/phortune/storage/PhortunePurchase.php
@@ -1,7 +1,7 @@
<?php
/**
- * A purchase represents a user buying something or a subscription to a plan.
+ * A purchase represents a user buying something.
*/
final class PhortunePurchase extends PhortuneDAO
implements PhabricatorPolicyInterface {
@@ -23,6 +23,7 @@
protected $metadata = array();
private $cart = self::ATTACHABLE;
+ private $product = self::ATTACHABLE;
public static function initializeNewPurchase(
PhabricatorUser $actor,
@@ -72,14 +73,32 @@
return $this->assertAttached($this->cart);
}
+ public function attachProduct(PhortuneProduct $product) {
+ $this->product = $product;
+ return $this;
+ }
+
+ public function getProduct() {
+ return $this->assertAttached($this->product);
+ }
+
public function getFullDisplayName() {
- return pht('Goods and/or Services');
+ return $this->getProduct()->getPurchaseName($this);
}
public function getTotalPriceAsCurrency() {
return $this->getBasePriceAsCurrency();
}
+ public function getMetadataValue($key, $default = null) {
+ return idx($this->metadata, $key, $default);
+ }
+
+ public function setMetadataValue($key, $value) {
+ $this->metadata[$key] = $value;
+ return $this;
+ }
+
/* -( PhabricatorPolicyInterface )----------------------------------------- */

File Metadata

Mime Type
text/plain
Expires
Fri, Jan 24, 9:29 AM (18 h, 52 m)
Storage Engine
blob
Storage Format
Encrypted (AES-256-CBC)
Storage Handle
7039838
Default Alt Text
D10635.id25543.diff (21 KB)

Event Timeline