1
0
Fork 0
mirror of https://we.phorge.it/source/phorge.git synced 2024-12-18 19:40:55 +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 (
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;

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',
'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',

View file

@ -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.

View file

@ -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',

View file

@ -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);
}

View file

@ -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',

View file

@ -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',

View file

@ -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();

View file

@ -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);

View file

@ -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),
);

View file

@ -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'))

View file

@ -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);
}

View file

@ -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();

View file

@ -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)

View file

@ -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}').");
}
}

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() {
$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;
}

View file

@ -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;
}

View file

@ -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()

View file

@ -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,

View file

@ -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())

View file

@ -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();
}

View file

@ -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);
}

View file

@ -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);
}

View file

@ -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());
}

View file

@ -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();

View file

@ -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 )----------------------------------------- */

View file

@ -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);
}
}

View file

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