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:
parent
3463ce8a51
commit
f86f9dc512
36 changed files with 241 additions and 213 deletions
|
@ -1,18 +1,18 @@
|
|||
CREATE TABLE {$NAMESPACE}_almanac.almanac_device (
|
||||
id INT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
|
||||
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,
|
||||
dateModified INT UNSIGNED NOT NULL,
|
||||
UNIQUE KEY `key_phid` (phid)
|
||||
) ENGINE=InnoDB, COLLATE {$COLLATE_TEXT};
|
||||
) ENGINE=InnoDB, COLLATE utf8_bin;
|
||||
|
||||
CREATE TABLE {$NAMESPACE}_almanac.almanac_deviceproperty (
|
||||
id INT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
|
||||
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,
|
||||
dateCreated INT UNSIGNED NOT NULL,
|
||||
dateModified INT UNSIGNED NOT NULL,
|
||||
KEY `key_device` (devicePHID, `key`)
|
||||
) ENGINE=InnoDB, COLLATE {$COLLATE_TEXT};
|
||||
) ENGINE=InnoDB, COLLATE utf8_bin;
|
||||
|
|
4
resources/sql/autopatches/20141004.currency.01.sql
Normal file
4
resources/sql/autopatches/20141004.currency.01.sql
Normal 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;
|
2
resources/sql/autopatches/20141004.currency.02.sql
Normal file
2
resources/sql/autopatches/20141004.currency.02.sql
Normal file
|
@ -0,0 +1,2 @@
|
|||
ALTER TABLE {$NAMESPACE}_phortune.phortune_account
|
||||
DROP balanceInCents;
|
4
resources/sql/autopatches/20141004.currency.03.sql
Normal file
4
resources/sql/autopatches/20141004.currency.03.sql
Normal 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;
|
13
resources/sql/autopatches/20141004.currency.04.sql
Normal file
13
resources/sql/autopatches/20141004.currency.04.sql
Normal 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;
|
8
resources/sql/autopatches/20141004.currency.05.sql
Normal file
8
resources/sql/autopatches/20141004.currency.05.sql
Normal 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;
|
2
resources/sql/autopatches/20141004.currency.06.sql
Normal file
2
resources/sql/autopatches/20141004.currency.06.sql
Normal file
|
@ -0,0 +1,2 @@
|
|||
ALTER TABLE {$NAMESPACE}_phortune.phortune_product
|
||||
DROP productType;
|
4
resources/sql/autopatches/20141004.harborliskcounter.sql
Normal file
4
resources/sql/autopatches/20141004.harborliskcounter.sql
Normal 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;
|
|
@ -1716,6 +1716,7 @@ phutil_register_library_map(array(
|
|||
'PhabricatorLipsumManagementWorkflow' => 'applications/lipsum/management/PhabricatorLipsumManagementWorkflow.php',
|
||||
'PhabricatorLipsumMondrianArtist' => 'applications/lipsum/image/PhabricatorLipsumMondrianArtist.php',
|
||||
'PhabricatorLiskDAO' => 'infrastructure/storage/lisk/PhabricatorLiskDAO.php',
|
||||
'PhabricatorLiskSerializer' => 'infrastructure/storage/lisk/PhabricatorLiskSerializer.php',
|
||||
'PhabricatorLocalDiskFileStorageEngine' => 'applications/files/engine/PhabricatorLocalDiskFileStorageEngine.php',
|
||||
'PhabricatorLocalTimeTestCase' => 'view/__tests__/PhabricatorLocalTimeTestCase.php',
|
||||
'PhabricatorLogoutController' => 'applications/auth/controller/PhabricatorLogoutController.php',
|
||||
|
@ -2561,6 +2562,7 @@ phutil_register_library_map(array(
|
|||
'PhortuneController' => 'applications/phortune/controller/PhortuneController.php',
|
||||
'PhortuneCreditCardForm' => 'applications/phortune/view/PhortuneCreditCardForm.php',
|
||||
'PhortuneCurrency' => 'applications/phortune/currency/PhortuneCurrency.php',
|
||||
'PhortuneCurrencySerializer' => 'applications/phortune/currency/PhortuneCurrencySerializer.php',
|
||||
'PhortuneCurrencyTestCase' => 'applications/phortune/currency/__tests__/PhortuneCurrencyTestCase.php',
|
||||
'PhortuneDAO' => 'applications/phortune/storage/PhortuneDAO.php',
|
||||
'PhortuneErrCode' => 'applications/phortune/constants/PhortuneErrCode.php',
|
||||
|
@ -5591,6 +5593,7 @@ phutil_register_library_map(array(
|
|||
'PhortuneChargeQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
|
||||
'PhortuneController' => 'PhabricatorController',
|
||||
'PhortuneCurrency' => 'Phobject',
|
||||
'PhortuneCurrencySerializer' => 'PhabricatorLiskSerializer',
|
||||
'PhortuneCurrencyTestCase' => 'PhabricatorTestCase',
|
||||
'PhortuneDAO' => 'PhabricatorLiskDAO',
|
||||
'PhortuneErrCode' => 'PhortuneConstants',
|
||||
|
|
|
@ -57,7 +57,7 @@ final class FundInitiativeBackController
|
|||
$backer = FundBacker::initializeNewBacker($viewer)
|
||||
->setInitiativePHID($initiative->getPHID())
|
||||
->attachInitiative($initiative)
|
||||
->setAmountInCents($currency->getValue())
|
||||
->setAmountAsCurrency($currency)
|
||||
->save();
|
||||
|
||||
// TODO: Here, we'd create a purchase and cart.
|
||||
|
|
|
@ -128,8 +128,7 @@ final class FundBackerSearchEngine
|
|||
foreach ($backers as $backer) {
|
||||
$backer_handle = $handles[$backer->getBackerPHID()];
|
||||
|
||||
$currency = PhortuneCurrency::newFromUSDCents(
|
||||
$backer->getAmountInCents());
|
||||
$currency = $backer->getAmount();
|
||||
|
||||
$header = pht(
|
||||
'%s for %s',
|
||||
|
|
|
@ -7,7 +7,7 @@ final class FundBacker extends FundDAO
|
|||
|
||||
protected $initiativePHID;
|
||||
protected $backerPHID;
|
||||
protected $amountInCents;
|
||||
protected $amountAsCurrency;
|
||||
protected $status;
|
||||
protected $properties = array();
|
||||
|
||||
|
@ -28,9 +28,12 @@ final class FundBacker extends FundDAO
|
|||
self::CONFIG_SERIALIZATION => array(
|
||||
'properties' => self::SERIALIZATION_JSON,
|
||||
),
|
||||
self::CONFIG_APPLICATION_SERIALIZERS => array(
|
||||
'amountAsCurrency' => new PhortuneCurrencySerializer(),
|
||||
),
|
||||
self::CONFIG_COLUMN_SCHEMA => array(
|
||||
'status' => 'text32',
|
||||
'amountInCents' => 'uint32',
|
||||
'amountAsCurrency' => 'text64',
|
||||
),
|
||||
self::CONFIG_KEY_SCHEMA => array(
|
||||
'key_initiative' => array(
|
||||
|
@ -47,11 +50,6 @@ final class FundBacker extends FundDAO
|
|||
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) {
|
||||
return idx($this->properties, $key, $default);
|
||||
}
|
||||
|
|
|
@ -5,6 +5,23 @@ final class HarbormasterSchemaSpec extends PhabricatorConfigSchemaSpec {
|
|||
public function buildSchemata() {
|
||||
$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(
|
||||
id(new HarbormasterBuildable())->getApplicationName(),
|
||||
'harbormaster_buildlogchunk',
|
||||
|
|
|
@ -51,7 +51,7 @@ final class PhortuneAccountViewController extends PhortuneController {
|
|||
->setObject($account)
|
||||
->setUser($user);
|
||||
|
||||
$properties->addProperty(pht('Balance'), $account->getBalanceInCents());
|
||||
$properties->addProperty(pht('Balance'), '-');
|
||||
$properties->setActionList($actions);
|
||||
|
||||
$payment_methods = $this->buildPaymentMethodsSection($account);
|
||||
|
@ -189,8 +189,7 @@ final class PhortuneAccountViewController extends PhortuneController {
|
|||
foreach ($cart->getPurchases() as $purchase) {
|
||||
$id = $purchase->getID();
|
||||
|
||||
$price = $purchase->getTotalPriceInCents();
|
||||
$price = PhortuneCurrency::newFromUSDCents($price)->formatForDisplay();
|
||||
$price = $purchase->getTotalPriceAsCurrency()->formatForDisplay();
|
||||
|
||||
$purchase_link = phutil_tag(
|
||||
'a',
|
||||
|
|
|
@ -59,7 +59,7 @@ final class PhortuneCartCheckoutController
|
|||
->setAuthorPHID($viewer->getPHID())
|
||||
->setPaymentProviderKey($provider->getProviderKey())
|
||||
->setPaymentMethodPHID($method->getPHID())
|
||||
->setAmountInCents($cart->getTotalPriceInCents())
|
||||
->setAmountAsCurrency($cart->getTotalPriceAsCurrency())
|
||||
->setStatus(PhortuneCharge::STATUS_PENDING);
|
||||
|
||||
$charge->openTransaction();
|
||||
|
|
|
@ -6,18 +6,13 @@ abstract class PhortuneCartController
|
|||
protected function buildCartContents(PhortuneCart $cart) {
|
||||
|
||||
$rows = array();
|
||||
$total = 0;
|
||||
foreach ($cart->getPurchases() as $purchase) {
|
||||
$rows[] = array(
|
||||
$purchase->getFullDisplayName(),
|
||||
PhortuneCurrency::newFromUSDCents($purchase->getBasePriceInCents())
|
||||
->formatForDisplay(),
|
||||
$purchase->getBasePriceAsCurrency()->formatForDisplay(),
|
||||
$purchase->getQuantity(),
|
||||
PhortuneCurrency::newFromUSDCents($purchase->getTotalPriceInCents())
|
||||
->formatForDisplay(),
|
||||
$purchase->getTotalPriceAsCurrency()->formatForDisplay(),
|
||||
);
|
||||
|
||||
$total += $purchase->getTotalPriceInCents();
|
||||
}
|
||||
|
||||
$rows[] = array(
|
||||
|
@ -25,7 +20,7 @@ abstract class PhortuneCartController
|
|||
'',
|
||||
'',
|
||||
phutil_tag('strong', array(),
|
||||
PhortuneCurrency::newFromUSDCents($total)->formatForDisplay()),
|
||||
$cart->getTotalPriceAsCurrency()->formatForDisplay()),
|
||||
);
|
||||
|
||||
$table = new AphrontTableView($rows);
|
||||
|
|
|
@ -73,8 +73,7 @@ abstract class PhortuneController extends PhabricatorController {
|
|||
$cart_href,
|
||||
$charge->getPaymentProviderKey(),
|
||||
$charge->getPaymentMethodPHID(),
|
||||
PhortuneCurrency::newFromUSDCents($charge->getAmountInCents())
|
||||
->formatForDisplay(),
|
||||
$charge->getAmountAsCurrency()->formatForDisplay(),
|
||||
$charge->getStatus(),
|
||||
phabricator_datetime($charge->getDateCreated(), $viewer),
|
||||
);
|
||||
|
|
|
@ -25,19 +25,16 @@ final class PhortuneProductEditController extends PhabricatorController {
|
|||
$cancel_uri = $this->getApplicationURI(
|
||||
'product/view/'.$this->productID.'/');
|
||||
} else {
|
||||
$product = new PhortuneProduct();
|
||||
$product = PhortuneProduct::initializeNewProduct();
|
||||
$is_create = true;
|
||||
$cancel_uri = $this->getApplicationURI('product/');
|
||||
}
|
||||
|
||||
$v_name = $product->getProductName();
|
||||
$v_type = $product->getProductType();
|
||||
$v_price = (int)$product->getPriceInCents();
|
||||
$display_price = PhortuneCurrency::newFromUSDCents($v_price)
|
||||
->formatForDisplay();
|
||||
$v_price = $product->getPriceAsCurrency()->formatForDisplay();
|
||||
$display_price = $v_price;
|
||||
|
||||
$e_name = true;
|
||||
$e_type = null;
|
||||
$e_price = true;
|
||||
$errors = array();
|
||||
|
||||
|
@ -50,21 +47,10 @@ final class PhortuneProductEditController extends PhabricatorController {
|
|||
$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');
|
||||
try {
|
||||
$v_price = PhortuneCurrency::newFromUserInput($user, $display_price)
|
||||
->getValue();
|
||||
->serializeForStorage();
|
||||
$e_price = null;
|
||||
} catch (Exception $ex) {
|
||||
$errors[] = pht('Price should be formatted as: $1.23');
|
||||
|
@ -78,10 +64,6 @@ final class PhortuneProductEditController extends PhabricatorController {
|
|||
->setTransactionType(PhortuneProductTransaction::TYPE_NAME)
|
||||
->setNewValue($v_name);
|
||||
|
||||
$xactions[] = id(new PhortuneProductTransaction())
|
||||
->setTransactionType(PhortuneProductTransaction::TYPE_TYPE)
|
||||
->setNewValue($v_type);
|
||||
|
||||
$xactions[] = id(new PhortuneProductTransaction())
|
||||
->setTransactionType(PhortuneProductTransaction::TYPE_PRICE)
|
||||
->setNewValue($v_price);
|
||||
|
@ -111,14 +93,6 @@ final class PhortuneProductEditController extends PhabricatorController {
|
|||
->setName('name')
|
||||
->setValue($v_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(
|
||||
id(new AphrontFormTextControl())
|
||||
->setLabel(pht('Price'))
|
||||
|
|
|
@ -32,15 +32,13 @@ final class PhortuneProductListController extends PhabricatorController {
|
|||
$view_uri = $this->getApplicationURI(
|
||||
'product/view/'.$product->getID().'/');
|
||||
|
||||
$price = $product->getPriceInCents();
|
||||
$price = $product->getPriceAsCurrency();
|
||||
|
||||
$item = id(new PHUIObjectItemView())
|
||||
->setObjectName($product->getID())
|
||||
->setHeader($product->getProductName())
|
||||
->setHref($view_uri)
|
||||
->addAttribute(
|
||||
PhortuneCurrency::newFromUSDCents($price)->formatForDisplay())
|
||||
->addAttribute($product->getTypeName());
|
||||
->addAttribute($price->formatForDisplay());
|
||||
|
||||
$product_list->addItem($item);
|
||||
}
|
||||
|
|
|
@ -49,10 +49,9 @@ final class PhortuneProductPurchaseController
|
|||
$purchase->setAccountPHID($account->getPHID());
|
||||
$purchase->setAuthorPHID($user->getPHID());
|
||||
$purchase->setCartPHID($cart->getPHID());
|
||||
$purchase->setBasePriceInCents($product->getPriceInCents());
|
||||
$purchase->setBasePriceAsCurrency($product->getPriceAsCurrency());
|
||||
$purchase->setQuantity(1);
|
||||
$purchase->setTotalPriceInCents(
|
||||
$purchase->getBasePriceInCents() * $purchase->getQuantity());
|
||||
|
||||
$purchase->setStatus(PhortunePurchase::STATUS_PENDING);
|
||||
$purchase->save();
|
||||
|
||||
|
|
|
@ -60,11 +60,9 @@ final class PhortuneProductViewController extends PhortuneController {
|
|||
$properties = id(new PHUIPropertyListView())
|
||||
->setUser($user)
|
||||
->setActionList($actions)
|
||||
->addProperty(pht('Type'), $product->getTypeName())
|
||||
->addProperty(
|
||||
pht('Price'),
|
||||
PhortuneCurrency::newFromUSDCents($product->getPriceInCents())
|
||||
->formatForDisplay());
|
||||
$product->getPriceAsCurrency()->formatForDisplay());
|
||||
|
||||
$xactions = id(new PhortuneProductTransactionQuery())
|
||||
->setViewer($user)
|
||||
|
|
|
@ -9,7 +9,20 @@ final class PhortuneCurrency extends Phobject {
|
|||
// 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) {
|
||||
// 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;
|
||||
$ok = preg_match(
|
||||
'/^([-$]*(?:\d+)?(?:[.]\d{0,2})?)(?:\s+([A-Z]+))?$/',
|
||||
|
@ -34,7 +47,7 @@ final class PhortuneCurrency extends Phobject {
|
|||
$value = (float)$value;
|
||||
$value = (int)round(100 * $value);
|
||||
|
||||
$currency = idx($matches, 2, 'USD');
|
||||
$currency = idx($matches, 2, $default);
|
||||
if ($currency) {
|
||||
switch ($currency) {
|
||||
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->value = $value;
|
||||
|
@ -56,31 +73,34 @@ final class PhortuneCurrency extends Phobject {
|
|||
assert_instances_of($list, 'PhortuneCurrency');
|
||||
|
||||
$total = 0;
|
||||
$currency = null;
|
||||
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.
|
||||
$total += $item->getValue();
|
||||
}
|
||||
|
||||
return PhortuneCurrency::newFromUSDCents($total);
|
||||
}
|
||||
|
||||
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;
|
||||
return PhortuneCurrency::newFromValueAndCurrency(
|
||||
$total,
|
||||
self::getDefaultCurrency());
|
||||
}
|
||||
|
||||
public function formatForDisplay() {
|
||||
$bare = $this->formatBareValue();
|
||||
return '$'.$bare.' USD';
|
||||
return '$'.$bare.' '.$this->currency;
|
||||
}
|
||||
|
||||
public function serializeForStorage() {
|
||||
return $this->formatBareValue().' '.$this->currency;
|
||||
}
|
||||
|
||||
public function formatBareValue() {
|
||||
|
@ -88,8 +108,8 @@ final class PhortuneCurrency extends Phobject {
|
|||
case 'USD':
|
||||
return sprintf('%.02f', $this->value / 100);
|
||||
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}').");
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
||||
}
|
|
@ -4,18 +4,18 @@ final class PhortuneCurrencyTestCase extends PhabricatorTestCase {
|
|||
|
||||
public function testCurrencyFormatForDisplay() {
|
||||
$map = array(
|
||||
0 => '$0.00 USD',
|
||||
1 => '$0.01 USD',
|
||||
100 => '$1.00 USD',
|
||||
-123 => '$-1.23 USD',
|
||||
5000000 => '$50000.00 USD',
|
||||
'0' => '$0.00 USD',
|
||||
'.01' => '$0.01 USD',
|
||||
'1.00' => '$1.00 USD',
|
||||
'-1.23' => '$-1.23 USD',
|
||||
'50000.00' => '$50000.00 USD',
|
||||
);
|
||||
|
||||
foreach ($map as $input => $expect) {
|
||||
$this->assertEqual(
|
||||
$expect,
|
||||
PhortuneCurrency::newFromUSDCents($input)->formatForDisplay(),
|
||||
"formatForDisplay({$input})");
|
||||
PhortuneCurrency::newFromString($input, 'USD')->formatForDisplay(),
|
||||
"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!
|
||||
|
||||
$map = array(
|
||||
0 => '0.00',
|
||||
1 => '0.01',
|
||||
100 => '1.00',
|
||||
-123 => '-1.23',
|
||||
5000000 => '50000.00',
|
||||
'0' => '0.00',
|
||||
'.01' => '0.01',
|
||||
'1.00' => '1.00',
|
||||
'-1.23' => '-1.23',
|
||||
'50000.00' => '50000.00',
|
||||
);
|
||||
|
||||
foreach ($map as $input => $expect) {
|
||||
$this->assertEqual(
|
||||
$expect,
|
||||
PhortuneCurrency::newFromUSDCents($input)->formatBareValue(),
|
||||
"formatBareValue({$input})");
|
||||
PhortuneCurrency::newFromString($input, 'USD')->formatBareValue(),
|
||||
"newFromString({$input})->formatBareValue()");
|
||||
}
|
||||
}
|
||||
|
||||
public function testCurrencyFromUserInput() {
|
||||
public function testCurrencyFromString() {
|
||||
|
||||
$map = array(
|
||||
'1.00' => 100,
|
||||
|
@ -57,17 +57,15 @@ final class PhortuneCurrencyTestCase extends PhabricatorTestCase {
|
|||
'$.99 USD' => 99,
|
||||
);
|
||||
|
||||
$user = new PhabricatorUser();
|
||||
|
||||
foreach ($map as $input => $expect) {
|
||||
$this->assertEqual(
|
||||
$expect,
|
||||
PhortuneCurrency::newFromUserInput($user, $input)->getValue(),
|
||||
"newFromUserInput({$input})->getValue()");
|
||||
PhortuneCurrency::newFromString($input, 'USD')->getValue(),
|
||||
"newFromString({$input})->getValue()");
|
||||
}
|
||||
}
|
||||
|
||||
public function testInvalidCurrencyFromUserInput() {
|
||||
public function testInvalidCurrencyFromString() {
|
||||
$map = array(
|
||||
'--1',
|
||||
'$$1',
|
||||
|
@ -77,12 +75,10 @@ final class PhortuneCurrencyTestCase extends PhabricatorTestCase {
|
|||
'1 dollar',
|
||||
);
|
||||
|
||||
$user = new PhabricatorUser();
|
||||
|
||||
foreach ($map as $input) {
|
||||
$caught = null;
|
||||
try {
|
||||
PhortuneCurrency::newFromUserInput($user, $input);
|
||||
PhortuneCurrency::newFromString($input, 'USD');
|
||||
} catch (Exception $ex) {
|
||||
$caught = $ex;
|
||||
}
|
||||
|
|
|
@ -16,7 +16,6 @@ final class PhortuneProductEditor
|
|||
$types = parent::getTransactionTypes();
|
||||
|
||||
$types[] = PhortuneProductTransaction::TYPE_NAME;
|
||||
$types[] = PhortuneProductTransaction::TYPE_TYPE;
|
||||
$types[] = PhortuneProductTransaction::TYPE_PRICE;
|
||||
|
||||
return $types;
|
||||
|
@ -29,10 +28,8 @@ final class PhortuneProductEditor
|
|||
switch ($xaction->getTransactionType()) {
|
||||
case PhortuneProductTransaction::TYPE_NAME:
|
||||
return $object->getProductName();
|
||||
case PhortuneProductTransaction::TYPE_TYPE:
|
||||
return $object->getProductType();
|
||||
case PhortuneProductTransaction::TYPE_PRICE:
|
||||
return $object->getPriceInCents();
|
||||
return $object->getPriceAsCurrency()->serializeForStorage();
|
||||
}
|
||||
return parent::getCustomTransactionOldValue($object, $xaction);
|
||||
}
|
||||
|
@ -42,7 +39,6 @@ final class PhortuneProductEditor
|
|||
PhabricatorApplicationTransaction $xaction) {
|
||||
switch ($xaction->getTransactionType()) {
|
||||
case PhortuneProductTransaction::TYPE_NAME:
|
||||
case PhortuneProductTransaction::TYPE_TYPE:
|
||||
case PhortuneProductTransaction::TYPE_PRICE:
|
||||
return $xaction->getNewValue();
|
||||
}
|
||||
|
@ -56,11 +52,9 @@ final class PhortuneProductEditor
|
|||
case PhortuneProductTransaction::TYPE_NAME:
|
||||
$object->setProductName($xaction->getNewValue());
|
||||
return;
|
||||
case PhortuneProductTransaction::TYPE_TYPE:
|
||||
$object->setProductType($xaction->getNewValue());
|
||||
return;
|
||||
case PhortuneProductTransaction::TYPE_PRICE:
|
||||
$object->setPriceInCents($xaction->getNewValue());
|
||||
$object->setPriceAsCurrency(
|
||||
PhortuneCurrency::newFromString($xaction->getNewValue()));
|
||||
return;
|
||||
}
|
||||
return parent::applyCustomInternalTransaction($object, $xaction);
|
||||
|
@ -71,7 +65,6 @@ final class PhortuneProductEditor
|
|||
PhabricatorApplicationTransaction $xaction) {
|
||||
switch ($xaction->getTransactionType()) {
|
||||
case PhortuneProductTransaction::TYPE_NAME:
|
||||
case PhortuneProductTransaction::TYPE_TYPE:
|
||||
case PhortuneProductTransaction::TYPE_PRICE:
|
||||
return;
|
||||
}
|
||||
|
|
|
@ -93,8 +93,7 @@ final class PhortunePaypalPaymentProvider extends PhortunePaymentProvider {
|
|||
'cartID' => $cart->getID(),
|
||||
));
|
||||
|
||||
$total_in_cents = $cart->getTotalPriceInCents();
|
||||
$price = PhortuneCurrency::newFromUSDCents($total_in_cents);
|
||||
$price = $cart->getTotalPriceAsCurrency();
|
||||
|
||||
$result = $this
|
||||
->newPaypalAPICall()
|
||||
|
|
|
@ -47,10 +47,12 @@ final class PhortuneStripePaymentProvider extends PhortunePaymentProvider {
|
|||
$root = dirname(phutil_get_library_root('phabricator'));
|
||||
require_once $root.'/externals/stripe-php/lib/Stripe.php';
|
||||
|
||||
$price = $charge->getAmountAsCurrency();
|
||||
|
||||
$secret_key = $this->getSecretKey();
|
||||
$params = array(
|
||||
'amount' => $charge->getAmountInCents(),
|
||||
'currency' => 'usd',
|
||||
'amount' => $price->getValue(),
|
||||
'currency' => $price->getCurrency(),
|
||||
'customer' => $method->getMetadataValue('stripe.customerID'),
|
||||
'description' => $charge->getPHID(),
|
||||
'capture' => true,
|
||||
|
|
|
@ -116,8 +116,7 @@ final class PhortuneWePayPaymentProvider extends PhortunePaymentProvider {
|
|||
'cartID' => $cart->getID(),
|
||||
));
|
||||
|
||||
$total_in_cents = $cart->getTotalPriceInCents();
|
||||
$price = PhortuneCurrency::newFromUSDCents($total_in_cents);
|
||||
$price = $cart->getTotalPriceAsCurrency();
|
||||
|
||||
$params = array(
|
||||
'account_id' => $this->getWePayAccountID(),
|
||||
|
@ -176,10 +175,12 @@ final class PhortuneWePayPaymentProvider extends PhortunePaymentProvider {
|
|||
$result->state));
|
||||
}
|
||||
|
||||
$currency = PhortuneCurrency::newFromString($checkout->gross, 'USD');
|
||||
|
||||
$unguarded = AphrontWriteGuard::beginScopedUnguardedWrites();
|
||||
|
||||
$charge = id(new PhortuneCharge())
|
||||
->setAmountInCents((int)$checkout->gross * 100)
|
||||
->setAmountAsCurrency($currency)
|
||||
->setAccountPHID($cart->getAccount()->getPHID())
|
||||
->setAuthorPHID($viewer->getPHID())
|
||||
->setPaymentProviderKey($this->getProviderKey())
|
||||
|
|
|
@ -10,7 +10,6 @@ final class PhortuneAccount extends PhortuneDAO
|
|||
implements PhabricatorPolicyInterface {
|
||||
|
||||
protected $name;
|
||||
protected $balanceInCents = 0;
|
||||
|
||||
private $memberPHIDs = self::ATTACHABLE;
|
||||
|
||||
|
@ -19,7 +18,6 @@ final class PhortuneAccount extends PhortuneDAO
|
|||
self::CONFIG_AUX_PHID => true,
|
||||
self::CONFIG_COLUMN_SCHEMA => array(
|
||||
'name' => 'text255',
|
||||
'balanceInCents' => 'sint64',
|
||||
),
|
||||
) + parent::getConfiguration();
|
||||
}
|
||||
|
|
|
@ -56,14 +56,13 @@ final class PhortuneCart extends PhortuneDAO
|
|||
return $this->assertAttached($this->account);
|
||||
}
|
||||
|
||||
public function getTotalPriceInCents() {
|
||||
public function getTotalPriceAsCurrency() {
|
||||
$prices = array();
|
||||
foreach ($this->getPurchases() as $purchase) {
|
||||
$prices[] = PhortuneCurrency::newFromUSDCents(
|
||||
$purchase->getTotalPriceInCents());
|
||||
$prices[] = $purchase->getTotalPriceAsCurrency();
|
||||
}
|
||||
|
||||
return PhortuneCurrency::newFromList($prices)->getValue();
|
||||
return PhortuneCurrency::newFromList($prices);
|
||||
}
|
||||
|
||||
|
||||
|
|
|
@ -20,7 +20,7 @@ final class PhortuneCharge extends PhortuneDAO
|
|||
protected $cartPHID;
|
||||
protected $paymentProviderKey;
|
||||
protected $paymentMethodPHID;
|
||||
protected $amountInCents;
|
||||
protected $amountAsCurrency;
|
||||
protected $status;
|
||||
protected $metadata = array();
|
||||
|
||||
|
@ -33,10 +33,13 @@ final class PhortuneCharge extends PhortuneDAO
|
|||
self::CONFIG_SERIALIZATION => array(
|
||||
'metadata' => self::SERIALIZATION_JSON,
|
||||
),
|
||||
self::CONFIG_APPLICATION_SERIALIZERS => array(
|
||||
'amountAsCurrency' => new PhortuneCurrencySerializer(),
|
||||
),
|
||||
self::CONFIG_COLUMN_SCHEMA => array(
|
||||
'paymentProviderKey' => 'text128',
|
||||
'paymentMethodPHID' => 'phid?',
|
||||
'amountInCents' => 'sint32',
|
||||
'amountAsCurrency' => 'text64',
|
||||
'status' => 'text32',
|
||||
),
|
||||
self::CONFIG_KEY_SCHEMA => array(
|
||||
|
@ -55,11 +58,6 @@ final class PhortuneCharge extends PhortuneDAO
|
|||
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) {
|
||||
return idx($this->metadata, $key, $default);
|
||||
}
|
||||
|
|
|
@ -1,24 +1,13 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* A product is something users can purchase. It may be a one-time purchase,
|
||||
* or a plan which is billed monthly.
|
||||
* A product is something users can purchase.
|
||||
*/
|
||||
final class PhortuneProduct extends PhortuneDAO
|
||||
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 $productType;
|
||||
protected $status = self::STATUS_ACTIVE;
|
||||
protected $priceInCents;
|
||||
protected $billingIntervalInMonths;
|
||||
protected $trialPeriodInDays;
|
||||
protected $priceAsCurrency;
|
||||
protected $metadata;
|
||||
|
||||
public function getConfiguration() {
|
||||
|
@ -27,19 +16,16 @@ final class PhortuneProduct extends PhortuneDAO
|
|||
self::CONFIG_SERIALIZATION => array(
|
||||
'metadata' => self::SERIALIZATION_JSON,
|
||||
),
|
||||
self::CONFIG_APPLICATION_SERIALIZERS => array(
|
||||
'priceAsCurrency' => new PhortuneCurrencySerializer(),
|
||||
),
|
||||
self::CONFIG_COLUMN_SCHEMA => array(
|
||||
'productName' => 'text255',
|
||||
'productType' => 'text64',
|
||||
'status' => 'text64',
|
||||
'priceInCents' => 'sint64',
|
||||
'priceAsCurrency' => 'text64',
|
||||
'billingIntervalInMonths' => 'uint32?',
|
||||
'trialPeriodInDays' => 'uint32?',
|
||||
),
|
||||
self::CONFIG_KEY_SCHEMA => array(
|
||||
'key_status' => array(
|
||||
'columns' => array('status'),
|
||||
),
|
||||
),
|
||||
) + parent::getConfiguration();
|
||||
}
|
||||
|
||||
|
@ -48,24 +34,9 @@ final class PhortuneProduct extends PhortuneDAO
|
|||
PhabricatorPHIDConstants::PHID_TYPE_PDCT);
|
||||
}
|
||||
|
||||
public static function getTypeMap() {
|
||||
return array(
|
||||
self::TYPE_BILL_ONCE => pht('Product (Charged Once)'),
|
||||
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();
|
||||
}
|
||||
public static function initializeNewProduct() {
|
||||
return id(new PhortuneProduct())
|
||||
->setPriceAsCurrency(PhortuneCurrency::newEmptyCurrency());
|
||||
}
|
||||
|
||||
|
||||
|
|
|
@ -4,7 +4,6 @@ final class PhortuneProductTransaction
|
|||
extends PhabricatorApplicationTransaction {
|
||||
|
||||
const TYPE_NAME = 'product:name';
|
||||
const TYPE_TYPE = 'product:type';
|
||||
const TYPE_PRICE = 'product:price';
|
||||
|
||||
public function getApplicationName() {
|
||||
|
@ -44,33 +43,18 @@ final class PhortuneProductTransaction
|
|||
return pht(
|
||||
'%s set product price to %s.',
|
||||
$this->renderHandleLink($author_phid),
|
||||
PhortuneCurrency::newFromUSDCents($new)
|
||||
PhortuneCurrency::newFromString($new)
|
||||
->formatForDisplay());
|
||||
} else {
|
||||
return pht(
|
||||
'%s changed product price from %s to %s.',
|
||||
$this->renderHandleLink($author_phid),
|
||||
PhortuneCurrency::newFromUSDCents($old)
|
||||
PhortuneCurrency::newFromString($old)
|
||||
->formatForDisplay(),
|
||||
PhortuneCurrency::newFromUSDCents($new)
|
||||
PhortuneCurrency::newFromString($new)
|
||||
->formatForDisplay());
|
||||
}
|
||||
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();
|
||||
|
|
|
@ -17,9 +17,8 @@ final class PhortunePurchase extends PhortuneDAO
|
|||
protected $accountPHID;
|
||||
protected $authorPHID;
|
||||
protected $cartPHID;
|
||||
protected $basePriceInCents;
|
||||
protected $basePriceAsCurrency;
|
||||
protected $quantity;
|
||||
protected $totalPriceInCents;
|
||||
protected $status;
|
||||
protected $metadata;
|
||||
|
||||
|
@ -31,11 +30,13 @@ final class PhortunePurchase extends PhortuneDAO
|
|||
self::CONFIG_SERIALIZATION => array(
|
||||
'metadata' => self::SERIALIZATION_JSON,
|
||||
),
|
||||
self::CONFIG_APPLICATION_SERIALIZERS => array(
|
||||
'basePriceAsCurrency' => new PhortuneCurrencySerializer(),
|
||||
),
|
||||
self::CONFIG_COLUMN_SCHEMA => array(
|
||||
'cartPHID' => 'phid?',
|
||||
'basePriceInCents' => 'sint32',
|
||||
'basePriceAsCurrency' => 'text64',
|
||||
'quantity' => 'uint32',
|
||||
'totalPriceInCents' => 'sint32',
|
||||
'status' => 'text32',
|
||||
),
|
||||
self::CONFIG_KEY_SCHEMA => array(
|
||||
|
@ -60,16 +61,14 @@ final class PhortunePurchase extends PhortuneDAO
|
|||
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() {
|
||||
return pht('Goods and/or Services');
|
||||
}
|
||||
|
||||
public function getTotalPriceAsCurrency() {
|
||||
return $this->getBasePriceAsCurrency();
|
||||
}
|
||||
|
||||
|
||||
/* -( PhabricatorPolicyInterface )----------------------------------------- */
|
||||
|
||||
|
|
|
@ -8,6 +8,7 @@ abstract class PhabricatorLiskDAO extends LiskDAO {
|
|||
private static $namespaceStack = array();
|
||||
|
||||
const ATTACHABLE = '<attachable>';
|
||||
const CONFIG_APPLICATION_SERIALIZERS = 'phabricator/serializers';
|
||||
|
||||
/* -( Configuring Storage )------------------------------------------------ */
|
||||
|
||||
|
@ -209,14 +210,35 @@ abstract class PhabricatorLiskDAO extends LiskDAO {
|
|||
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
|
||||
// infrastructure objects here, like edges, transactions, custom field
|
||||
// storage, flags, Phrequent tracking, tokens, etc. This doesn't need to
|
||||
// be exhaustive, but we can get a lot of it pretty easily.
|
||||
static $custom;
|
||||
if ($custom === null) {
|
||||
$custom = $this->getConfigOption(self::CONFIG_APPLICATION_SERIALIZERS);
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
|
|
@ -0,0 +1,8 @@
|
|||
<?php
|
||||
|
||||
abstract class PhabricatorLiskSerializer {
|
||||
|
||||
abstract public function willReadValue($value);
|
||||
abstract public function willWriteValue($value);
|
||||
|
||||
}
|
Loading…
Reference in a new issue