Page Menu
Home
Phabricator
Search
Configure Global Search
Log In
Files
F14763112
D10635.id25543.diff
No One
Temporary
Actions
View File
Edit File
Delete File
View Transforms
Subscribe
Mute Notifications
Award Token
Flag For Later
Size
21 KB
Referenced Files
None
Subscribers
None
D10635.id25543.diff
View Options
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
Details
Attached
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)
Attached To
Mode
D10635: Add more structure to Phortune product purchasing flow
Attached
Detach File
Event Timeline
Log In to Comment