1
0
Fork 0
mirror of https://we.phorge.it/source/phorge.git synced 2024-11-18 12:52:42 +01:00

Make Currency a more formal type

Summary:
Ref T2787. Phortune currently stores a bunch of stuff as `...inUSDCents`. This ends up being pretty cumbersome and I worry it will create a huge headache down the road (and possibly not that far off if we do Coinbase/Bitcoin soon). Even now, it's more of a pain than I figured it would be.

Instead:

  - Provide an application-level serialization mechanism.
  - Provide currency serialization.
  - Store currency in an abstract way (currently, as "1.23 USD") that can handle currencies in the future.
  - Change all `...inUSDCents` to `..asCurrency`.
  - This generally simplifies all the application code.
  - Also remove some columns which don't make sense or don't make sense anymore. Notably, `Product` is going to get more abstract and mostly be provided by applications.

Test Plan:
  - Created a new product.
  - Purchased a product.
  - Backed an initiative.
  - Ran unit tests.

Reviewers: btrahan

Reviewed By: btrahan

Subscribers: epriestley

Maniphest Tasks: T2787

Differential Revision: https://secure.phabricator.com/D10633
This commit is contained in:
epriestley 2014-10-06 10:26:48 -07:00
parent 3463ce8a51
commit f86f9dc512
36 changed files with 241 additions and 213 deletions

View file

@ -1,18 +1,18 @@
CREATE TABLE {$NAMESPACE}_almanac.almanac_device ( CREATE TABLE {$NAMESPACE}_almanac.almanac_device (
id INT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY, id INT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
phid VARBINARY(64) NOT NULL, phid VARBINARY(64) NOT NULL,
name VARCHAR(255) NOT NULL COLLATE {$COLLATE_TEXT}, name VARCHAR(255) NOT NULL COLLATE utf8_bin,
dateCreated INT UNSIGNED NOT NULL, dateCreated INT UNSIGNED NOT NULL,
dateModified INT UNSIGNED NOT NULL, dateModified INT UNSIGNED NOT NULL,
UNIQUE KEY `key_phid` (phid) UNIQUE KEY `key_phid` (phid)
) ENGINE=InnoDB, COLLATE {$COLLATE_TEXT}; ) ENGINE=InnoDB, COLLATE utf8_bin;
CREATE TABLE {$NAMESPACE}_almanac.almanac_deviceproperty ( CREATE TABLE {$NAMESPACE}_almanac.almanac_deviceproperty (
id INT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY, id INT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
devicePHID VARBINARY(64) NOT NULL, devicePHID VARBINARY(64) NOT NULL,
`key` VARCHAR(128) NOT NULL COLLATE {$COLLATE_TEXT}, `key` VARCHAR(128) NOT NULL COLLATE utf8_bin,
value LONGTEXT NOT NULL, value LONGTEXT NOT NULL,
dateCreated INT UNSIGNED NOT NULL, dateCreated INT UNSIGNED NOT NULL,
dateModified INT UNSIGNED NOT NULL, dateModified INT UNSIGNED NOT NULL,
KEY `key_device` (devicePHID, `key`) KEY `key_device` (devicePHID, `key`)
) ENGINE=InnoDB, COLLATE {$COLLATE_TEXT}; ) ENGINE=InnoDB, COLLATE utf8_bin;

View file

@ -0,0 +1,4 @@
TRUNCATE TABLE {$NAMESPACE}_fund.fund_backer;
ALTER TABLE {$NAMESPACE}_fund.fund_backer
CHANGE amountInCents amountAsCurrency VARCHAR(64) NOT NULL COLLATE utf8_bin;

View file

@ -0,0 +1,2 @@
ALTER TABLE {$NAMESPACE}_phortune.phortune_account
DROP balanceInCents;

View file

@ -0,0 +1,4 @@
TRUNCATE {$NAMESPACE}_phortune.phortune_charge;
ALTER TABLE {$NAMESPACE}_phortune.phortune_charge
CHANGE amountInCents amountAsCurrency VARCHAR(64) NOT NULL COLLATE utf8_bin;

View file

@ -0,0 +1,13 @@
TRUNCATE {$NAMESPACE}_phortune.phortune_product;
ALTER TABLE {$NAMESPACE}_phortune.phortune_product
DROP status;
ALTER TABLE {$NAMESPACE}_phortune.phortune_product
DROP billingIntervalInMonths;
ALTER TABLE {$NAMESPACE}_phortune.phortune_product
DROP trialPeriodInDays;
ALTER TABLE {$NAMESPACE}_phortune.phortune_product
CHANGE priceInCents priceAsCurrency VARCHAR(64) NOT NULL collate utf8_bin;

View file

@ -0,0 +1,8 @@
TRUNCATE {$NAMESPACE}_phortune.phortune_purchase;
ALTER TABLE {$NAMESPACE}_phortune.phortune_purchase
DROP totalPriceInCents;
ALTER TABLE {$NAMESPACE}_phortune.phortune_purchase
CHANGE basePriceInCents basePriceAsCurrency VARCHAR(64)
NOT NULL collate utf8_bin;

View file

@ -0,0 +1,2 @@
ALTER TABLE {$NAMESPACE}_phortune.phortune_product
DROP productType;

View file

@ -0,0 +1,4 @@
CREATE TABLE `{$NAMESPACE}_harbormaster`.`lisk_counter` (
counterName VARCHAR(64) COLLATE utf8_bin PRIMARY KEY,
counterValue BIGINT UNSIGNED NOT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

View file

@ -1716,6 +1716,7 @@ phutil_register_library_map(array(
'PhabricatorLipsumManagementWorkflow' => 'applications/lipsum/management/PhabricatorLipsumManagementWorkflow.php', 'PhabricatorLipsumManagementWorkflow' => 'applications/lipsum/management/PhabricatorLipsumManagementWorkflow.php',
'PhabricatorLipsumMondrianArtist' => 'applications/lipsum/image/PhabricatorLipsumMondrianArtist.php', 'PhabricatorLipsumMondrianArtist' => 'applications/lipsum/image/PhabricatorLipsumMondrianArtist.php',
'PhabricatorLiskDAO' => 'infrastructure/storage/lisk/PhabricatorLiskDAO.php', 'PhabricatorLiskDAO' => 'infrastructure/storage/lisk/PhabricatorLiskDAO.php',
'PhabricatorLiskSerializer' => 'infrastructure/storage/lisk/PhabricatorLiskSerializer.php',
'PhabricatorLocalDiskFileStorageEngine' => 'applications/files/engine/PhabricatorLocalDiskFileStorageEngine.php', 'PhabricatorLocalDiskFileStorageEngine' => 'applications/files/engine/PhabricatorLocalDiskFileStorageEngine.php',
'PhabricatorLocalTimeTestCase' => 'view/__tests__/PhabricatorLocalTimeTestCase.php', 'PhabricatorLocalTimeTestCase' => 'view/__tests__/PhabricatorLocalTimeTestCase.php',
'PhabricatorLogoutController' => 'applications/auth/controller/PhabricatorLogoutController.php', 'PhabricatorLogoutController' => 'applications/auth/controller/PhabricatorLogoutController.php',
@ -2561,6 +2562,7 @@ phutil_register_library_map(array(
'PhortuneController' => 'applications/phortune/controller/PhortuneController.php', 'PhortuneController' => 'applications/phortune/controller/PhortuneController.php',
'PhortuneCreditCardForm' => 'applications/phortune/view/PhortuneCreditCardForm.php', 'PhortuneCreditCardForm' => 'applications/phortune/view/PhortuneCreditCardForm.php',
'PhortuneCurrency' => 'applications/phortune/currency/PhortuneCurrency.php', 'PhortuneCurrency' => 'applications/phortune/currency/PhortuneCurrency.php',
'PhortuneCurrencySerializer' => 'applications/phortune/currency/PhortuneCurrencySerializer.php',
'PhortuneCurrencyTestCase' => 'applications/phortune/currency/__tests__/PhortuneCurrencyTestCase.php', 'PhortuneCurrencyTestCase' => 'applications/phortune/currency/__tests__/PhortuneCurrencyTestCase.php',
'PhortuneDAO' => 'applications/phortune/storage/PhortuneDAO.php', 'PhortuneDAO' => 'applications/phortune/storage/PhortuneDAO.php',
'PhortuneErrCode' => 'applications/phortune/constants/PhortuneErrCode.php', 'PhortuneErrCode' => 'applications/phortune/constants/PhortuneErrCode.php',
@ -5591,6 +5593,7 @@ phutil_register_library_map(array(
'PhortuneChargeQuery' => 'PhabricatorCursorPagedPolicyAwareQuery', 'PhortuneChargeQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
'PhortuneController' => 'PhabricatorController', 'PhortuneController' => 'PhabricatorController',
'PhortuneCurrency' => 'Phobject', 'PhortuneCurrency' => 'Phobject',
'PhortuneCurrencySerializer' => 'PhabricatorLiskSerializer',
'PhortuneCurrencyTestCase' => 'PhabricatorTestCase', 'PhortuneCurrencyTestCase' => 'PhabricatorTestCase',
'PhortuneDAO' => 'PhabricatorLiskDAO', 'PhortuneDAO' => 'PhabricatorLiskDAO',
'PhortuneErrCode' => 'PhortuneConstants', 'PhortuneErrCode' => 'PhortuneConstants',

View file

@ -57,7 +57,7 @@ final class FundInitiativeBackController
$backer = FundBacker::initializeNewBacker($viewer) $backer = FundBacker::initializeNewBacker($viewer)
->setInitiativePHID($initiative->getPHID()) ->setInitiativePHID($initiative->getPHID())
->attachInitiative($initiative) ->attachInitiative($initiative)
->setAmountInCents($currency->getValue()) ->setAmountAsCurrency($currency)
->save(); ->save();
// TODO: Here, we'd create a purchase and cart. // TODO: Here, we'd create a purchase and cart.

View file

@ -128,8 +128,7 @@ final class FundBackerSearchEngine
foreach ($backers as $backer) { foreach ($backers as $backer) {
$backer_handle = $handles[$backer->getBackerPHID()]; $backer_handle = $handles[$backer->getBackerPHID()];
$currency = PhortuneCurrency::newFromUSDCents( $currency = $backer->getAmount();
$backer->getAmountInCents());
$header = pht( $header = pht(
'%s for %s', '%s for %s',

View file

@ -7,7 +7,7 @@ final class FundBacker extends FundDAO
protected $initiativePHID; protected $initiativePHID;
protected $backerPHID; protected $backerPHID;
protected $amountInCents; protected $amountAsCurrency;
protected $status; protected $status;
protected $properties = array(); protected $properties = array();
@ -28,9 +28,12 @@ final class FundBacker extends FundDAO
self::CONFIG_SERIALIZATION => array( self::CONFIG_SERIALIZATION => array(
'properties' => self::SERIALIZATION_JSON, 'properties' => self::SERIALIZATION_JSON,
), ),
self::CONFIG_APPLICATION_SERIALIZERS => array(
'amountAsCurrency' => new PhortuneCurrencySerializer(),
),
self::CONFIG_COLUMN_SCHEMA => array( self::CONFIG_COLUMN_SCHEMA => array(
'status' => 'text32', 'status' => 'text32',
'amountInCents' => 'uint32', 'amountAsCurrency' => 'text64',
), ),
self::CONFIG_KEY_SCHEMA => array( self::CONFIG_KEY_SCHEMA => array(
'key_initiative' => array( 'key_initiative' => array(
@ -47,11 +50,6 @@ final class FundBacker extends FundDAO
return PhabricatorPHID::generateNewPHID(FundBackerPHIDType::TYPECONST); return PhabricatorPHID::generateNewPHID(FundBackerPHIDType::TYPECONST);
} }
protected function didReadData() {
// The payment processing code is strict about types.
$this->amountInCents = (int)$this->amountInCents;
}
public function getProperty($key, $default = null) { public function getProperty($key, $default = null) {
return idx($this->properties, $key, $default); return idx($this->properties, $key, $default);
} }

View file

@ -5,6 +5,23 @@ final class HarbormasterSchemaSpec extends PhabricatorConfigSchemaSpec {
public function buildSchemata() { public function buildSchemata() {
$this->buildEdgeSchemata(new HarbormasterBuildable()); $this->buildEdgeSchemata(new HarbormasterBuildable());
// NOTE: This table is not used by any Harbormaster objects, but is used
// by unit tests.
$this->buildRawSchema(
id(new HarbormasterObject())->getApplicationName(),
PhabricatorLiskDAO::COUNTER_TABLE_NAME,
array(
'counterName' => 'text32',
'counterValue' => 'id64',
),
array(
'PRIMARY' => array(
'columns' => array('counterName'),
'unique' => true,
),
));
$this->buildRawSchema( $this->buildRawSchema(
id(new HarbormasterBuildable())->getApplicationName(), id(new HarbormasterBuildable())->getApplicationName(),
'harbormaster_buildlogchunk', 'harbormaster_buildlogchunk',

View file

@ -51,7 +51,7 @@ final class PhortuneAccountViewController extends PhortuneController {
->setObject($account) ->setObject($account)
->setUser($user); ->setUser($user);
$properties->addProperty(pht('Balance'), $account->getBalanceInCents()); $properties->addProperty(pht('Balance'), '-');
$properties->setActionList($actions); $properties->setActionList($actions);
$payment_methods = $this->buildPaymentMethodsSection($account); $payment_methods = $this->buildPaymentMethodsSection($account);
@ -189,8 +189,7 @@ final class PhortuneAccountViewController extends PhortuneController {
foreach ($cart->getPurchases() as $purchase) { foreach ($cart->getPurchases() as $purchase) {
$id = $purchase->getID(); $id = $purchase->getID();
$price = $purchase->getTotalPriceInCents(); $price = $purchase->getTotalPriceAsCurrency()->formatForDisplay();
$price = PhortuneCurrency::newFromUSDCents($price)->formatForDisplay();
$purchase_link = phutil_tag( $purchase_link = phutil_tag(
'a', 'a',

View file

@ -59,7 +59,7 @@ final class PhortuneCartCheckoutController
->setAuthorPHID($viewer->getPHID()) ->setAuthorPHID($viewer->getPHID())
->setPaymentProviderKey($provider->getProviderKey()) ->setPaymentProviderKey($provider->getProviderKey())
->setPaymentMethodPHID($method->getPHID()) ->setPaymentMethodPHID($method->getPHID())
->setAmountInCents($cart->getTotalPriceInCents()) ->setAmountAsCurrency($cart->getTotalPriceAsCurrency())
->setStatus(PhortuneCharge::STATUS_PENDING); ->setStatus(PhortuneCharge::STATUS_PENDING);
$charge->openTransaction(); $charge->openTransaction();

View file

@ -6,18 +6,13 @@ abstract class PhortuneCartController
protected function buildCartContents(PhortuneCart $cart) { protected function buildCartContents(PhortuneCart $cart) {
$rows = array(); $rows = array();
$total = 0;
foreach ($cart->getPurchases() as $purchase) { foreach ($cart->getPurchases() as $purchase) {
$rows[] = array( $rows[] = array(
$purchase->getFullDisplayName(), $purchase->getFullDisplayName(),
PhortuneCurrency::newFromUSDCents($purchase->getBasePriceInCents()) $purchase->getBasePriceAsCurrency()->formatForDisplay(),
->formatForDisplay(),
$purchase->getQuantity(), $purchase->getQuantity(),
PhortuneCurrency::newFromUSDCents($purchase->getTotalPriceInCents()) $purchase->getTotalPriceAsCurrency()->formatForDisplay(),
->formatForDisplay(),
); );
$total += $purchase->getTotalPriceInCents();
} }
$rows[] = array( $rows[] = array(
@ -25,7 +20,7 @@ abstract class PhortuneCartController
'', '',
'', '',
phutil_tag('strong', array(), phutil_tag('strong', array(),
PhortuneCurrency::newFromUSDCents($total)->formatForDisplay()), $cart->getTotalPriceAsCurrency()->formatForDisplay()),
); );
$table = new AphrontTableView($rows); $table = new AphrontTableView($rows);

View file

@ -73,8 +73,7 @@ abstract class PhortuneController extends PhabricatorController {
$cart_href, $cart_href,
$charge->getPaymentProviderKey(), $charge->getPaymentProviderKey(),
$charge->getPaymentMethodPHID(), $charge->getPaymentMethodPHID(),
PhortuneCurrency::newFromUSDCents($charge->getAmountInCents()) $charge->getAmountAsCurrency()->formatForDisplay(),
->formatForDisplay(),
$charge->getStatus(), $charge->getStatus(),
phabricator_datetime($charge->getDateCreated(), $viewer), phabricator_datetime($charge->getDateCreated(), $viewer),
); );

View file

@ -25,19 +25,16 @@ final class PhortuneProductEditController extends PhabricatorController {
$cancel_uri = $this->getApplicationURI( $cancel_uri = $this->getApplicationURI(
'product/view/'.$this->productID.'/'); 'product/view/'.$this->productID.'/');
} else { } else {
$product = new PhortuneProduct(); $product = PhortuneProduct::initializeNewProduct();
$is_create = true; $is_create = true;
$cancel_uri = $this->getApplicationURI('product/'); $cancel_uri = $this->getApplicationURI('product/');
} }
$v_name = $product->getProductName(); $v_name = $product->getProductName();
$v_type = $product->getProductType(); $v_price = $product->getPriceAsCurrency()->formatForDisplay();
$v_price = (int)$product->getPriceInCents(); $display_price = $v_price;
$display_price = PhortuneCurrency::newFromUSDCents($v_price)
->formatForDisplay();
$e_name = true; $e_name = true;
$e_type = null;
$e_price = true; $e_price = true;
$errors = array(); $errors = array();
@ -50,21 +47,10 @@ final class PhortuneProductEditController extends PhabricatorController {
$e_name = null; $e_name = null;
} }
if ($is_create) {
$v_type = $request->getStr('type');
$type_map = PhortuneProduct::getTypeMap();
if (empty($type_map[$v_type])) {
$e_type = pht('Invalid');
$errors[] = pht('Product type is invalid.');
} else {
$e_type = null;
}
}
$display_price = $request->getStr('price'); $display_price = $request->getStr('price');
try { try {
$v_price = PhortuneCurrency::newFromUserInput($user, $display_price) $v_price = PhortuneCurrency::newFromUserInput($user, $display_price)
->getValue(); ->serializeForStorage();
$e_price = null; $e_price = null;
} catch (Exception $ex) { } catch (Exception $ex) {
$errors[] = pht('Price should be formatted as: $1.23'); $errors[] = pht('Price should be formatted as: $1.23');
@ -78,10 +64,6 @@ final class PhortuneProductEditController extends PhabricatorController {
->setTransactionType(PhortuneProductTransaction::TYPE_NAME) ->setTransactionType(PhortuneProductTransaction::TYPE_NAME)
->setNewValue($v_name); ->setNewValue($v_name);
$xactions[] = id(new PhortuneProductTransaction())
->setTransactionType(PhortuneProductTransaction::TYPE_TYPE)
->setNewValue($v_type);
$xactions[] = id(new PhortuneProductTransaction()) $xactions[] = id(new PhortuneProductTransaction())
->setTransactionType(PhortuneProductTransaction::TYPE_PRICE) ->setTransactionType(PhortuneProductTransaction::TYPE_PRICE)
->setNewValue($v_price); ->setNewValue($v_price);
@ -111,14 +93,6 @@ final class PhortuneProductEditController extends PhabricatorController {
->setName('name') ->setName('name')
->setValue($v_name) ->setValue($v_name)
->setError($e_name)) ->setError($e_name))
->appendChild(
id(new AphrontFormSelectControl())
->setLabel(pht('Type'))
->setName('type')
->setValue($v_type)
->setError($e_type)
->setOptions(PhortuneProduct::getTypeMap())
->setDisabled(!$is_create))
->appendChild( ->appendChild(
id(new AphrontFormTextControl()) id(new AphrontFormTextControl())
->setLabel(pht('Price')) ->setLabel(pht('Price'))

View file

@ -32,15 +32,13 @@ final class PhortuneProductListController extends PhabricatorController {
$view_uri = $this->getApplicationURI( $view_uri = $this->getApplicationURI(
'product/view/'.$product->getID().'/'); 'product/view/'.$product->getID().'/');
$price = $product->getPriceInCents(); $price = $product->getPriceAsCurrency();
$item = id(new PHUIObjectItemView()) $item = id(new PHUIObjectItemView())
->setObjectName($product->getID()) ->setObjectName($product->getID())
->setHeader($product->getProductName()) ->setHeader($product->getProductName())
->setHref($view_uri) ->setHref($view_uri)
->addAttribute( ->addAttribute($price->formatForDisplay());
PhortuneCurrency::newFromUSDCents($price)->formatForDisplay())
->addAttribute($product->getTypeName());
$product_list->addItem($item); $product_list->addItem($item);
} }

View file

@ -49,10 +49,9 @@ final class PhortuneProductPurchaseController
$purchase->setAccountPHID($account->getPHID()); $purchase->setAccountPHID($account->getPHID());
$purchase->setAuthorPHID($user->getPHID()); $purchase->setAuthorPHID($user->getPHID());
$purchase->setCartPHID($cart->getPHID()); $purchase->setCartPHID($cart->getPHID());
$purchase->setBasePriceInCents($product->getPriceInCents()); $purchase->setBasePriceAsCurrency($product->getPriceAsCurrency());
$purchase->setQuantity(1); $purchase->setQuantity(1);
$purchase->setTotalPriceInCents(
$purchase->getBasePriceInCents() * $purchase->getQuantity());
$purchase->setStatus(PhortunePurchase::STATUS_PENDING); $purchase->setStatus(PhortunePurchase::STATUS_PENDING);
$purchase->save(); $purchase->save();

View file

@ -60,11 +60,9 @@ final class PhortuneProductViewController extends PhortuneController {
$properties = id(new PHUIPropertyListView()) $properties = id(new PHUIPropertyListView())
->setUser($user) ->setUser($user)
->setActionList($actions) ->setActionList($actions)
->addProperty(pht('Type'), $product->getTypeName())
->addProperty( ->addProperty(
pht('Price'), pht('Price'),
PhortuneCurrency::newFromUSDCents($product->getPriceInCents()) $product->getPriceAsCurrency()->formatForDisplay());
->formatForDisplay());
$xactions = id(new PhortuneProductTransactionQuery()) $xactions = id(new PhortuneProductTransactionQuery())
->setViewer($user) ->setViewer($user)

View file

@ -9,7 +9,20 @@ final class PhortuneCurrency extends Phobject {
// Intentionally private. // Intentionally private.
} }
public static function getDefaultCurrency() {
return 'USD';
}
public static function newEmptyCurrency() {
return self::newFromString('0.00 USD');
}
public static function newFromUserInput(PhabricatorUser $user, $string) { public static function newFromUserInput(PhabricatorUser $user, $string) {
// Eventually, this might select a default currency based on user settings.
return self::newFromString($string, self::getDefaultCurrency());
}
public static function newFromString($string, $default = null) {
$matches = null; $matches = null;
$ok = preg_match( $ok = preg_match(
'/^([-$]*(?:\d+)?(?:[.]\d{0,2})?)(?:\s+([A-Z]+))?$/', '/^([-$]*(?:\d+)?(?:[.]\d{0,2})?)(?:\s+([A-Z]+))?$/',
@ -34,7 +47,7 @@ final class PhortuneCurrency extends Phobject {
$value = (float)$value; $value = (float)$value;
$value = (int)round(100 * $value); $value = (int)round(100 * $value);
$currency = idx($matches, 2, 'USD'); $currency = idx($matches, 2, $default);
if ($currency) { if ($currency) {
switch ($currency) { switch ($currency) {
case 'USD': case 'USD':
@ -44,6 +57,10 @@ final class PhortuneCurrency extends Phobject {
} }
} }
return self::newFromValueAndCurrency($value, $currency);
}
public static function newFromValueAndCurrency($value, $currency) {
$obj = new PhortuneCurrency(); $obj = new PhortuneCurrency();
$obj->value = $value; $obj->value = $value;
@ -56,31 +73,34 @@ final class PhortuneCurrency extends Phobject {
assert_instances_of($list, 'PhortuneCurrency'); assert_instances_of($list, 'PhortuneCurrency');
$total = 0; $total = 0;
$currency = null;
foreach ($list as $item) { foreach ($list as $item) {
if ($currency === null) {
$currency = $item->getCurrency();
} else if ($currency === $item->getCurrency()) {
// Adding a value denominated in the same currency, which is
// fine.
} else {
throw new Exception(
pht('Trying to sum a list of unlike currencies.'));
}
// TODO: This should check for integer overflows, etc. // TODO: This should check for integer overflows, etc.
$total += $item->getValue(); $total += $item->getValue();
} }
return PhortuneCurrency::newFromUSDCents($total); return PhortuneCurrency::newFromValueAndCurrency(
} $total,
self::getDefaultCurrency());
public static function newFromUSDCents($cents) {
if (!is_int($cents)) {
throw new Exception(
pht('USDCents value "%s" is not an integer!', $cents));
}
$obj = new PhortuneCurrency();
$obj->value = $cents;
$obj->currency = 'USD';
return $obj;
} }
public function formatForDisplay() { public function formatForDisplay() {
$bare = $this->formatBareValue(); $bare = $this->formatBareValue();
return '$'.$bare.' USD'; return '$'.$bare.' '.$this->currency;
}
public function serializeForStorage() {
return $this->formatBareValue().' '.$this->currency;
} }
public function formatBareValue() { public function formatBareValue() {
@ -88,8 +108,8 @@ final class PhortuneCurrency extends Phobject {
case 'USD': case 'USD':
return sprintf('%.02f', $this->value / 100); return sprintf('%.02f', $this->value / 100);
default: default:
throw new Exception('Unsupported currency!'); throw new Exception(
pht('Unsupported currency ("%s")!', $this->currency));
} }
} }
@ -105,4 +125,6 @@ final class PhortuneCurrency extends Phobject {
throw new Exception("Invalid currency format ('{$string}')."); throw new Exception("Invalid currency format ('{$string}').");
} }
} }

View file

@ -0,0 +1,20 @@
<?php
final class PhortuneCurrencySerializer extends PhabricatorLiskSerializer {
public function willReadValue($value) {
return PhortuneCurrency::newFromString($value);
}
public function willWriteValue($value) {
if (!($value instanceof PhortuneCurrency)) {
throw new Exception(
pht(
'Trying to save object with a currency column, but the column '.
'value is not a PhortuneCurrency object.'));
}
return $value->serializeForStorage();
}
}

View file

@ -4,18 +4,18 @@ final class PhortuneCurrencyTestCase extends PhabricatorTestCase {
public function testCurrencyFormatForDisplay() { public function testCurrencyFormatForDisplay() {
$map = array( $map = array(
0 => '$0.00 USD', '0' => '$0.00 USD',
1 => '$0.01 USD', '.01' => '$0.01 USD',
100 => '$1.00 USD', '1.00' => '$1.00 USD',
-123 => '$-1.23 USD', '-1.23' => '$-1.23 USD',
5000000 => '$50000.00 USD', '50000.00' => '$50000.00 USD',
); );
foreach ($map as $input => $expect) { foreach ($map as $input => $expect) {
$this->assertEqual( $this->assertEqual(
$expect, $expect,
PhortuneCurrency::newFromUSDCents($input)->formatForDisplay(), PhortuneCurrency::newFromString($input, 'USD')->formatForDisplay(),
"formatForDisplay({$input})"); "newFromString({$input})->formatForDisplay()");
} }
} }
@ -25,22 +25,22 @@ final class PhortuneCurrencyTestCase extends PhabricatorTestCase {
// NOTE: The PayPal API depends on the behavior of the bare value format! // NOTE: The PayPal API depends on the behavior of the bare value format!
$map = array( $map = array(
0 => '0.00', '0' => '0.00',
1 => '0.01', '.01' => '0.01',
100 => '1.00', '1.00' => '1.00',
-123 => '-1.23', '-1.23' => '-1.23',
5000000 => '50000.00', '50000.00' => '50000.00',
); );
foreach ($map as $input => $expect) { foreach ($map as $input => $expect) {
$this->assertEqual( $this->assertEqual(
$expect, $expect,
PhortuneCurrency::newFromUSDCents($input)->formatBareValue(), PhortuneCurrency::newFromString($input, 'USD')->formatBareValue(),
"formatBareValue({$input})"); "newFromString({$input})->formatBareValue()");
} }
} }
public function testCurrencyFromUserInput() { public function testCurrencyFromString() {
$map = array( $map = array(
'1.00' => 100, '1.00' => 100,
@ -57,17 +57,15 @@ final class PhortuneCurrencyTestCase extends PhabricatorTestCase {
'$.99 USD' => 99, '$.99 USD' => 99,
); );
$user = new PhabricatorUser();
foreach ($map as $input => $expect) { foreach ($map as $input => $expect) {
$this->assertEqual( $this->assertEqual(
$expect, $expect,
PhortuneCurrency::newFromUserInput($user, $input)->getValue(), PhortuneCurrency::newFromString($input, 'USD')->getValue(),
"newFromUserInput({$input})->getValue()"); "newFromString({$input})->getValue()");
} }
} }
public function testInvalidCurrencyFromUserInput() { public function testInvalidCurrencyFromString() {
$map = array( $map = array(
'--1', '--1',
'$$1', '$$1',
@ -77,12 +75,10 @@ final class PhortuneCurrencyTestCase extends PhabricatorTestCase {
'1 dollar', '1 dollar',
); );
$user = new PhabricatorUser();
foreach ($map as $input) { foreach ($map as $input) {
$caught = null; $caught = null;
try { try {
PhortuneCurrency::newFromUserInput($user, $input); PhortuneCurrency::newFromString($input, 'USD');
} catch (Exception $ex) { } catch (Exception $ex) {
$caught = $ex; $caught = $ex;
} }

View file

@ -16,7 +16,6 @@ final class PhortuneProductEditor
$types = parent::getTransactionTypes(); $types = parent::getTransactionTypes();
$types[] = PhortuneProductTransaction::TYPE_NAME; $types[] = PhortuneProductTransaction::TYPE_NAME;
$types[] = PhortuneProductTransaction::TYPE_TYPE;
$types[] = PhortuneProductTransaction::TYPE_PRICE; $types[] = PhortuneProductTransaction::TYPE_PRICE;
return $types; return $types;
@ -29,10 +28,8 @@ final class PhortuneProductEditor
switch ($xaction->getTransactionType()) { switch ($xaction->getTransactionType()) {
case PhortuneProductTransaction::TYPE_NAME: case PhortuneProductTransaction::TYPE_NAME:
return $object->getProductName(); return $object->getProductName();
case PhortuneProductTransaction::TYPE_TYPE:
return $object->getProductType();
case PhortuneProductTransaction::TYPE_PRICE: case PhortuneProductTransaction::TYPE_PRICE:
return $object->getPriceInCents(); return $object->getPriceAsCurrency()->serializeForStorage();
} }
return parent::getCustomTransactionOldValue($object, $xaction); return parent::getCustomTransactionOldValue($object, $xaction);
} }
@ -42,7 +39,6 @@ final class PhortuneProductEditor
PhabricatorApplicationTransaction $xaction) { PhabricatorApplicationTransaction $xaction) {
switch ($xaction->getTransactionType()) { switch ($xaction->getTransactionType()) {
case PhortuneProductTransaction::TYPE_NAME: case PhortuneProductTransaction::TYPE_NAME:
case PhortuneProductTransaction::TYPE_TYPE:
case PhortuneProductTransaction::TYPE_PRICE: case PhortuneProductTransaction::TYPE_PRICE:
return $xaction->getNewValue(); return $xaction->getNewValue();
} }
@ -56,11 +52,9 @@ final class PhortuneProductEditor
case PhortuneProductTransaction::TYPE_NAME: case PhortuneProductTransaction::TYPE_NAME:
$object->setProductName($xaction->getNewValue()); $object->setProductName($xaction->getNewValue());
return; return;
case PhortuneProductTransaction::TYPE_TYPE:
$object->setProductType($xaction->getNewValue());
return;
case PhortuneProductTransaction::TYPE_PRICE: case PhortuneProductTransaction::TYPE_PRICE:
$object->setPriceInCents($xaction->getNewValue()); $object->setPriceAsCurrency(
PhortuneCurrency::newFromString($xaction->getNewValue()));
return; return;
} }
return parent::applyCustomInternalTransaction($object, $xaction); return parent::applyCustomInternalTransaction($object, $xaction);
@ -71,7 +65,6 @@ final class PhortuneProductEditor
PhabricatorApplicationTransaction $xaction) { PhabricatorApplicationTransaction $xaction) {
switch ($xaction->getTransactionType()) { switch ($xaction->getTransactionType()) {
case PhortuneProductTransaction::TYPE_NAME: case PhortuneProductTransaction::TYPE_NAME:
case PhortuneProductTransaction::TYPE_TYPE:
case PhortuneProductTransaction::TYPE_PRICE: case PhortuneProductTransaction::TYPE_PRICE:
return; return;
} }

View file

@ -93,8 +93,7 @@ final class PhortunePaypalPaymentProvider extends PhortunePaymentProvider {
'cartID' => $cart->getID(), 'cartID' => $cart->getID(),
)); ));
$total_in_cents = $cart->getTotalPriceInCents(); $price = $cart->getTotalPriceAsCurrency();
$price = PhortuneCurrency::newFromUSDCents($total_in_cents);
$result = $this $result = $this
->newPaypalAPICall() ->newPaypalAPICall()

View file

@ -47,10 +47,12 @@ final class PhortuneStripePaymentProvider extends PhortunePaymentProvider {
$root = dirname(phutil_get_library_root('phabricator')); $root = dirname(phutil_get_library_root('phabricator'));
require_once $root.'/externals/stripe-php/lib/Stripe.php'; require_once $root.'/externals/stripe-php/lib/Stripe.php';
$price = $charge->getAmountAsCurrency();
$secret_key = $this->getSecretKey(); $secret_key = $this->getSecretKey();
$params = array( $params = array(
'amount' => $charge->getAmountInCents(), 'amount' => $price->getValue(),
'currency' => 'usd', 'currency' => $price->getCurrency(),
'customer' => $method->getMetadataValue('stripe.customerID'), 'customer' => $method->getMetadataValue('stripe.customerID'),
'description' => $charge->getPHID(), 'description' => $charge->getPHID(),
'capture' => true, 'capture' => true,

View file

@ -116,8 +116,7 @@ final class PhortuneWePayPaymentProvider extends PhortunePaymentProvider {
'cartID' => $cart->getID(), 'cartID' => $cart->getID(),
)); ));
$total_in_cents = $cart->getTotalPriceInCents(); $price = $cart->getTotalPriceAsCurrency();
$price = PhortuneCurrency::newFromUSDCents($total_in_cents);
$params = array( $params = array(
'account_id' => $this->getWePayAccountID(), 'account_id' => $this->getWePayAccountID(),
@ -176,10 +175,12 @@ final class PhortuneWePayPaymentProvider extends PhortunePaymentProvider {
$result->state)); $result->state));
} }
$currency = PhortuneCurrency::newFromString($checkout->gross, 'USD');
$unguarded = AphrontWriteGuard::beginScopedUnguardedWrites(); $unguarded = AphrontWriteGuard::beginScopedUnguardedWrites();
$charge = id(new PhortuneCharge()) $charge = id(new PhortuneCharge())
->setAmountInCents((int)$checkout->gross * 100) ->setAmountAsCurrency($currency)
->setAccountPHID($cart->getAccount()->getPHID()) ->setAccountPHID($cart->getAccount()->getPHID())
->setAuthorPHID($viewer->getPHID()) ->setAuthorPHID($viewer->getPHID())
->setPaymentProviderKey($this->getProviderKey()) ->setPaymentProviderKey($this->getProviderKey())

View file

@ -10,7 +10,6 @@ final class PhortuneAccount extends PhortuneDAO
implements PhabricatorPolicyInterface { implements PhabricatorPolicyInterface {
protected $name; protected $name;
protected $balanceInCents = 0;
private $memberPHIDs = self::ATTACHABLE; private $memberPHIDs = self::ATTACHABLE;
@ -19,7 +18,6 @@ final class PhortuneAccount extends PhortuneDAO
self::CONFIG_AUX_PHID => true, self::CONFIG_AUX_PHID => true,
self::CONFIG_COLUMN_SCHEMA => array( self::CONFIG_COLUMN_SCHEMA => array(
'name' => 'text255', 'name' => 'text255',
'balanceInCents' => 'sint64',
), ),
) + parent::getConfiguration(); ) + parent::getConfiguration();
} }

View file

@ -56,14 +56,13 @@ final class PhortuneCart extends PhortuneDAO
return $this->assertAttached($this->account); return $this->assertAttached($this->account);
} }
public function getTotalPriceInCents() { public function getTotalPriceAsCurrency() {
$prices = array(); $prices = array();
foreach ($this->getPurchases() as $purchase) { foreach ($this->getPurchases() as $purchase) {
$prices[] = PhortuneCurrency::newFromUSDCents( $prices[] = $purchase->getTotalPriceAsCurrency();
$purchase->getTotalPriceInCents());
} }
return PhortuneCurrency::newFromList($prices)->getValue(); return PhortuneCurrency::newFromList($prices);
} }

View file

@ -20,7 +20,7 @@ final class PhortuneCharge extends PhortuneDAO
protected $cartPHID; protected $cartPHID;
protected $paymentProviderKey; protected $paymentProviderKey;
protected $paymentMethodPHID; protected $paymentMethodPHID;
protected $amountInCents; protected $amountAsCurrency;
protected $status; protected $status;
protected $metadata = array(); protected $metadata = array();
@ -33,10 +33,13 @@ final class PhortuneCharge extends PhortuneDAO
self::CONFIG_SERIALIZATION => array( self::CONFIG_SERIALIZATION => array(
'metadata' => self::SERIALIZATION_JSON, 'metadata' => self::SERIALIZATION_JSON,
), ),
self::CONFIG_APPLICATION_SERIALIZERS => array(
'amountAsCurrency' => new PhortuneCurrencySerializer(),
),
self::CONFIG_COLUMN_SCHEMA => array( self::CONFIG_COLUMN_SCHEMA => array(
'paymentProviderKey' => 'text128', 'paymentProviderKey' => 'text128',
'paymentMethodPHID' => 'phid?', 'paymentMethodPHID' => 'phid?',
'amountInCents' => 'sint32', 'amountAsCurrency' => 'text64',
'status' => 'text32', 'status' => 'text32',
), ),
self::CONFIG_KEY_SCHEMA => array( self::CONFIG_KEY_SCHEMA => array(
@ -55,11 +58,6 @@ final class PhortuneCharge extends PhortuneDAO
PhabricatorPHIDConstants::PHID_TYPE_CHRG); 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) { public function getMetadataValue($key, $default = null) {
return idx($this->metadata, $key, $default); return idx($this->metadata, $key, $default);
} }

View file

@ -1,24 +1,13 @@
<?php <?php
/** /**
* A product is something users can purchase. It may be a one-time purchase, * A product is something users can purchase.
* or a plan which is billed monthly.
*/ */
final class PhortuneProduct extends PhortuneDAO final class PhortuneProduct extends PhortuneDAO
implements PhabricatorPolicyInterface { implements PhabricatorPolicyInterface {
const TYPE_BILL_ONCE = 'phortune:thing';
const TYPE_BILL_PLAN = 'phortune:plan';
const STATUS_ACTIVE = 'product:active';
const STATUS_DISABLED = 'product:disabled';
protected $productName; protected $productName;
protected $productType; protected $priceAsCurrency;
protected $status = self::STATUS_ACTIVE;
protected $priceInCents;
protected $billingIntervalInMonths;
protected $trialPeriodInDays;
protected $metadata; protected $metadata;
public function getConfiguration() { public function getConfiguration() {
@ -27,19 +16,16 @@ final class PhortuneProduct extends PhortuneDAO
self::CONFIG_SERIALIZATION => array( self::CONFIG_SERIALIZATION => array(
'metadata' => self::SERIALIZATION_JSON, 'metadata' => self::SERIALIZATION_JSON,
), ),
self::CONFIG_APPLICATION_SERIALIZERS => array(
'priceAsCurrency' => new PhortuneCurrencySerializer(),
),
self::CONFIG_COLUMN_SCHEMA => array( self::CONFIG_COLUMN_SCHEMA => array(
'productName' => 'text255', 'productName' => 'text255',
'productType' => 'text64',
'status' => 'text64', 'status' => 'text64',
'priceInCents' => 'sint64', 'priceAsCurrency' => 'text64',
'billingIntervalInMonths' => 'uint32?', 'billingIntervalInMonths' => 'uint32?',
'trialPeriodInDays' => 'uint32?', 'trialPeriodInDays' => 'uint32?',
), ),
self::CONFIG_KEY_SCHEMA => array(
'key_status' => array(
'columns' => array('status'),
),
),
) + parent::getConfiguration(); ) + parent::getConfiguration();
} }
@ -48,24 +34,9 @@ final class PhortuneProduct extends PhortuneDAO
PhabricatorPHIDConstants::PHID_TYPE_PDCT); PhabricatorPHIDConstants::PHID_TYPE_PDCT);
} }
public static function getTypeMap() { public static function initializeNewProduct() {
return array( return id(new PhortuneProduct())
self::TYPE_BILL_ONCE => pht('Product (Charged Once)'), ->setPriceAsCurrency(PhortuneCurrency::newEmptyCurrency());
self::TYPE_BILL_PLAN => pht('Flat Rate Plan (Charged Monthly)'),
);
}
public function getTypeName() {
return idx(self::getTypeMap(), $this->getProductType());
}
public function getPriceInCents() {
$price = parent::getPriceInCents();
if ($price === null) {
return $price;
} else {
return (int)parent::getPriceInCents();
}
} }

View file

@ -4,7 +4,6 @@ final class PhortuneProductTransaction
extends PhabricatorApplicationTransaction { extends PhabricatorApplicationTransaction {
const TYPE_NAME = 'product:name'; const TYPE_NAME = 'product:name';
const TYPE_TYPE = 'product:type';
const TYPE_PRICE = 'product:price'; const TYPE_PRICE = 'product:price';
public function getApplicationName() { public function getApplicationName() {
@ -44,33 +43,18 @@ final class PhortuneProductTransaction
return pht( return pht(
'%s set product price to %s.', '%s set product price to %s.',
$this->renderHandleLink($author_phid), $this->renderHandleLink($author_phid),
PhortuneCurrency::newFromUSDCents($new) PhortuneCurrency::newFromString($new)
->formatForDisplay()); ->formatForDisplay());
} else { } else {
return pht( return pht(
'%s changed product price from %s to %s.', '%s changed product price from %s to %s.',
$this->renderHandleLink($author_phid), $this->renderHandleLink($author_phid),
PhortuneCurrency::newFromUSDCents($old) PhortuneCurrency::newFromString($old)
->formatForDisplay(), ->formatForDisplay(),
PhortuneCurrency::newFromUSDCents($new) PhortuneCurrency::newFromString($new)
->formatForDisplay()); ->formatForDisplay());
} }
break; break;
case self::TYPE_TYPE:
$map = PhortuneProduct::getTypeMap();
if ($old === null) {
return pht(
'%s set product type to "%s".',
$this->renderHandleLink($author_phid),
$map[$new]);
} else {
return pht(
'%s changed product type from "%s" to "%s".',
$this->renderHandleLink($author_phid),
$map[$old],
$map[$new]);
}
break;
} }
return parent::getTitle(); return parent::getTitle();

View file

@ -17,9 +17,8 @@ final class PhortunePurchase extends PhortuneDAO
protected $accountPHID; protected $accountPHID;
protected $authorPHID; protected $authorPHID;
protected $cartPHID; protected $cartPHID;
protected $basePriceInCents; protected $basePriceAsCurrency;
protected $quantity; protected $quantity;
protected $totalPriceInCents;
protected $status; protected $status;
protected $metadata; protected $metadata;
@ -31,11 +30,13 @@ final class PhortunePurchase extends PhortuneDAO
self::CONFIG_SERIALIZATION => array( self::CONFIG_SERIALIZATION => array(
'metadata' => self::SERIALIZATION_JSON, 'metadata' => self::SERIALIZATION_JSON,
), ),
self::CONFIG_APPLICATION_SERIALIZERS => array(
'basePriceAsCurrency' => new PhortuneCurrencySerializer(),
),
self::CONFIG_COLUMN_SCHEMA => array( self::CONFIG_COLUMN_SCHEMA => array(
'cartPHID' => 'phid?', 'cartPHID' => 'phid?',
'basePriceInCents' => 'sint32', 'basePriceAsCurrency' => 'text64',
'quantity' => 'uint32', 'quantity' => 'uint32',
'totalPriceInCents' => 'sint32',
'status' => 'text32', 'status' => 'text32',
), ),
self::CONFIG_KEY_SCHEMA => array( self::CONFIG_KEY_SCHEMA => array(
@ -60,16 +61,14 @@ final class PhortunePurchase extends PhortuneDAO
return $this->assertAttached($this->cart); 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;
}
public function getFullDisplayName() { public function getFullDisplayName() {
return pht('Goods and/or Services'); return pht('Goods and/or Services');
} }
public function getTotalPriceAsCurrency() {
return $this->getBasePriceAsCurrency();
}
/* -( PhabricatorPolicyInterface )----------------------------------------- */ /* -( PhabricatorPolicyInterface )----------------------------------------- */

View file

@ -8,6 +8,7 @@ abstract class PhabricatorLiskDAO extends LiskDAO {
private static $namespaceStack = array(); private static $namespaceStack = array();
const ATTACHABLE = '<attachable>'; const ATTACHABLE = '<attachable>';
const CONFIG_APPLICATION_SERIALIZERS = 'phabricator/serializers';
/* -( Configuring Storage )------------------------------------------------ */ /* -( Configuring Storage )------------------------------------------------ */
@ -209,14 +210,35 @@ abstract class PhabricatorLiskDAO extends LiskDAO {
return phutil_utf8ize($string); return phutil_utf8ize($string);
} }
public function delete() { protected function willReadData(array &$data) {
parent::willReadData($data);
// TODO: We should make some reasonable effort to destroy related static $custom;
// infrastructure objects here, like edges, transactions, custom field if ($custom === null) {
// storage, flags, Phrequent tracking, tokens, etc. This doesn't need to $custom = $this->getConfigOption(self::CONFIG_APPLICATION_SERIALIZERS);
// be exhaustive, but we can get a lot of it pretty easily.
return parent::delete();
} }
if ($custom) {
foreach ($custom as $key => $serializer) {
$data[$key] = $serializer->willReadValue($data[$key]);
}
}
}
protected function willWriteData(array &$data) {
static $custom;
if ($custom === null) {
$custom = $this->getConfigOption(self::CONFIG_APPLICATION_SERIALIZERS);
}
if ($custom) {
foreach ($custom as $key => $serializer) {
$data[$key] = $serializer->willWriteValue($data[$key]);
}
}
parent::willWriteData($data);
}
} }

View file

@ -0,0 +1,8 @@
<?php
abstract class PhabricatorLiskSerializer {
abstract public function willReadValue($value);
abstract public function willWriteValue($value);
}