mirror of
https://we.phorge.it/source/phorge.git
synced 2024-12-23 22:10:55 +01:00
Add more structure to Phortune product purchasing flow
Summary: Ref T2787. When a user purchases a product in Phortune, transition the cart through a purchased state and invoke product callbacks so applications can respond to the workflow. Also shore up some stuff like preventing negative amounts of funding. Test Plan: Backed an initiative and saw it show up on the initiative after completing the purcahsing workflow. Reviewers: btrahan Reviewed By: btrahan Subscribers: epriestley Maniphest Tasks: T2787 Differential Revision: https://secure.phabricator.com/D10635
This commit is contained in:
parent
e9615b74a5
commit
35dc510e18
16 changed files with 352 additions and 11 deletions
|
@ -47,6 +47,7 @@ final class FundInitiativeBackController
|
|||
$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 @@ final class FundInitiativeBackController
|
|||
$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 @@ final class FundInitiativeBackController
|
|||
|
||||
$editor->applyTransactions($backer, $xactions);
|
||||
|
||||
$cart->activateCart();
|
||||
|
||||
return id(new AphrontRedirectResponse())
|
||||
->setURI($cart->getCheckoutURI());
|
||||
}
|
||||
|
|
|
@ -17,6 +17,7 @@ final class FundInitiativeEditor
|
|||
$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 @@ final class FundInitiativeEditor
|
|||
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 @@ final class FundInitiativeEditor
|
|||
case FundInitiativeTransaction::TYPE_NAME:
|
||||
case FundInitiativeTransaction::TYPE_DESCRIPTION:
|
||||
case FundInitiativeTransaction::TYPE_STATUS:
|
||||
case FundInitiativeTransaction::TYPE_BACKER:
|
||||
return $xaction->getNewValue();
|
||||
}
|
||||
|
||||
|
@ -66,6 +70,9 @@ final class FundInitiativeEditor
|
|||
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 @@ final class FundInitiativeEditor
|
|||
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;
|
||||
|
|
|
@ -4,13 +4,27 @@ final class FundBackerProduct extends PhortuneProductImplementation {
|
|||
|
||||
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 @@ final class FundBackerProduct extends PhortuneProductImplementation {
|
|||
$objects = array();
|
||||
foreach ($refs as $ref) {
|
||||
$object = id(new FundBackerProduct())
|
||||
->setViewer($viewer)
|
||||
->setInitiativePHID($ref);
|
||||
|
||||
$initiative = idx($initiatives, $ref);
|
||||
|
@ -61,4 +76,43 @@ final class FundBackerProduct extends PhortuneProductImplementation {
|
|||
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;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -5,6 +5,7 @@ final class FundBackerQuery
|
|||
|
||||
private $ids;
|
||||
private $phids;
|
||||
private $statuses;
|
||||
|
||||
private $initiativePHIDs;
|
||||
private $backerPHIDs;
|
||||
|
@ -19,6 +20,11 @@ final class FundBackerQuery
|
|||
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 @@ final class FundBackerQuery
|
|||
$this->backerPHIDs);
|
||||
}
|
||||
|
||||
if ($this->statuses !== null) {
|
||||
$where[] = qsprintf(
|
||||
$conn_r,
|
||||
'status IN (%Ls)',
|
||||
$this->statuses);
|
||||
}
|
||||
|
||||
return $this->formatWhereClause($where);
|
||||
}
|
||||
|
||||
|
|
|
@ -35,6 +35,8 @@ final class FundBackerSearchEngine
|
|||
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 @@ final class FundBackerSearchEngine
|
|||
foreach ($backers as $backer) {
|
||||
$backer_handle = $handles[$backer->getBackerPHID()];
|
||||
|
||||
$currency = $backer->getAmount();
|
||||
$currency = $backer->getAmountAsCurrency();
|
||||
|
||||
$header = pht(
|
||||
'%s for %s',
|
||||
|
|
|
@ -15,6 +15,7 @@ final class FundBacker extends FundDAO
|
|||
|
||||
const STATUS_NEW = 'new';
|
||||
const STATUS_IN_CART = 'in-cart';
|
||||
const STATUS_PURCHASED = 'purchased';
|
||||
|
||||
public static function initializeNewBacker(PhabricatorUser $actor) {
|
||||
return id(new FundBacker())
|
||||
|
|
|
@ -6,6 +6,7 @@ final class FundInitiativeTransaction
|
|||
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 @@ final class FundInitiativeTransaction
|
|||
$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 @@ final class FundInitiativeTransaction
|
|||
$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);
|
||||
|
|
|
@ -14,6 +14,7 @@ final class PhabricatorContentSource {
|
|||
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 @@ final class PhabricatorContentSource {
|
|||
self::SOURCE_DAEMON => pht('Daemons'),
|
||||
self::SOURCE_LIPSUM => pht('Lipsum'),
|
||||
self::SOURCE_UNKNOWN => pht('Old World'),
|
||||
self::SOURCE_PHORTUNE => pht('Phortune'),
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
@ -22,6 +22,42 @@ final class PhortuneCartCheckoutController
|
|||
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 @@ final class PhortuneCartCheckoutController
|
|||
|
||||
$provider->applyCharge($method, $charge);
|
||||
|
||||
$cart->setStatus(PhortuneCart::STATUS_PURCHASED);
|
||||
$cart->save();
|
||||
$cart->didApplyCharge($charge);
|
||||
|
||||
$view_uri = $this->getApplicationURI('cart/'.$cart->getID().'/');
|
||||
|
||||
return id(new AphrontRedirectResponse())->setURI($view_uri);
|
||||
$done_uri = $cart->getDoneURI();
|
||||
return id(new AphrontRedirectResponse())->setURI($done_uri);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -125,6 +125,54 @@ final class PhortuneCurrency extends Phobject {
|
|||
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;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
|
|
@ -86,4 +86,60 @@ final class PhortuneCurrencyTestCase extends PhabricatorTestCase {
|
|||
}
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -10,4 +10,22 @@ abstract class PhortuneProductImplementation {
|
|||
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;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -54,6 +54,23 @@ final class PhortunePurchaseQuery
|
|||
$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;
|
||||
}
|
||||
|
||||
|
|
|
@ -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 @@ final class PhortuneCart extends PhortuneDAO
|
|||
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 @@ final class PhortuneCart extends PhortuneDAO
|
|||
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/';
|
||||
}
|
||||
|
|
|
@ -70,6 +70,14 @@ final class PhortuneProduct extends PhortuneDAO
|
|||
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 )----------------------------------------- */
|
||||
|
||||
|
|
|
@ -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 @@ final class PhortunePurchase extends PhortuneDAO
|
|||
protected $metadata = array();
|
||||
|
||||
private $cart = self::ATTACHABLE;
|
||||
private $product = self::ATTACHABLE;
|
||||
|
||||
public static function initializeNewPurchase(
|
||||
PhabricatorUser $actor,
|
||||
|
@ -72,14 +73,32 @@ final class PhortunePurchase extends PhortuneDAO
|
|||
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 )----------------------------------------- */
|
||||
|
||||
|
|
Loading…
Reference in a new issue