@recaptime-dev's working patches + fork for Phorge, a community fork of Phabricator. (Upstream dev and stable branches are at upstream/main and upstream/stable respectively.) hq.recaptime.dev/wiki/Phorge
phorge phabricator
1
fork

Configure Feed

Select the types of activity you want to include in your feed.

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

+241 -213
+4 -4
resources/sql/autopatches/20140902.almanacdevice.1.sql
··· 1 1 CREATE TABLE {$NAMESPACE}_almanac.almanac_device ( 2 2 id INT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY, 3 3 phid VARBINARY(64) NOT NULL, 4 - name VARCHAR(255) NOT NULL COLLATE {$COLLATE_TEXT}, 4 + name VARCHAR(255) NOT NULL COLLATE utf8_bin, 5 5 dateCreated INT UNSIGNED NOT NULL, 6 6 dateModified INT UNSIGNED NOT NULL, 7 7 UNIQUE KEY `key_phid` (phid) 8 - ) ENGINE=InnoDB, COLLATE {$COLLATE_TEXT}; 8 + ) ENGINE=InnoDB, COLLATE utf8_bin; 9 9 10 10 CREATE TABLE {$NAMESPACE}_almanac.almanac_deviceproperty ( 11 11 id INT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY, 12 12 devicePHID VARBINARY(64) NOT NULL, 13 - `key` VARCHAR(128) NOT NULL COLLATE {$COLLATE_TEXT}, 13 + `key` VARCHAR(128) NOT NULL COLLATE utf8_bin, 14 14 value LONGTEXT NOT NULL, 15 15 dateCreated INT UNSIGNED NOT NULL, 16 16 dateModified INT UNSIGNED NOT NULL, 17 17 KEY `key_device` (devicePHID, `key`) 18 - ) ENGINE=InnoDB, COLLATE {$COLLATE_TEXT}; 18 + ) ENGINE=InnoDB, COLLATE utf8_bin;
+4
resources/sql/autopatches/20141004.currency.01.sql
··· 1 + TRUNCATE TABLE {$NAMESPACE}_fund.fund_backer; 2 + 3 + ALTER TABLE {$NAMESPACE}_fund.fund_backer 4 + CHANGE amountInCents amountAsCurrency VARCHAR(64) NOT NULL COLLATE utf8_bin;
+2
resources/sql/autopatches/20141004.currency.02.sql
··· 1 + ALTER TABLE {$NAMESPACE}_phortune.phortune_account 2 + DROP balanceInCents;
+4
resources/sql/autopatches/20141004.currency.03.sql
··· 1 + TRUNCATE {$NAMESPACE}_phortune.phortune_charge; 2 + 3 + ALTER TABLE {$NAMESPACE}_phortune.phortune_charge 4 + CHANGE amountInCents amountAsCurrency VARCHAR(64) NOT NULL COLLATE utf8_bin;
+13
resources/sql/autopatches/20141004.currency.04.sql
··· 1 + TRUNCATE {$NAMESPACE}_phortune.phortune_product; 2 + 3 + ALTER TABLE {$NAMESPACE}_phortune.phortune_product 4 + DROP status; 5 + 6 + ALTER TABLE {$NAMESPACE}_phortune.phortune_product 7 + DROP billingIntervalInMonths; 8 + 9 + ALTER TABLE {$NAMESPACE}_phortune.phortune_product 10 + DROP trialPeriodInDays; 11 + 12 + ALTER TABLE {$NAMESPACE}_phortune.phortune_product 13 + CHANGE priceInCents priceAsCurrency VARCHAR(64) NOT NULL collate utf8_bin;
+8
resources/sql/autopatches/20141004.currency.05.sql
··· 1 + TRUNCATE {$NAMESPACE}_phortune.phortune_purchase; 2 + 3 + ALTER TABLE {$NAMESPACE}_phortune.phortune_purchase 4 + DROP totalPriceInCents; 5 + 6 + ALTER TABLE {$NAMESPACE}_phortune.phortune_purchase 7 + CHANGE basePriceInCents basePriceAsCurrency VARCHAR(64) 8 + NOT NULL collate utf8_bin;
+2
resources/sql/autopatches/20141004.currency.06.sql
··· 1 + ALTER TABLE {$NAMESPACE}_phortune.phortune_product 2 + DROP productType;
+4
resources/sql/autopatches/20141004.harborliskcounter.sql
··· 1 + CREATE TABLE `{$NAMESPACE}_harbormaster`.`lisk_counter` ( 2 + counterName VARCHAR(64) COLLATE utf8_bin PRIMARY KEY, 3 + counterValue BIGINT UNSIGNED NOT NULL 4 + ) ENGINE=InnoDB DEFAULT CHARSET=utf8;
+3
src/__phutil_library_map__.php
··· 1716 1716 'PhabricatorLipsumManagementWorkflow' => 'applications/lipsum/management/PhabricatorLipsumManagementWorkflow.php', 1717 1717 'PhabricatorLipsumMondrianArtist' => 'applications/lipsum/image/PhabricatorLipsumMondrianArtist.php', 1718 1718 'PhabricatorLiskDAO' => 'infrastructure/storage/lisk/PhabricatorLiskDAO.php', 1719 + 'PhabricatorLiskSerializer' => 'infrastructure/storage/lisk/PhabricatorLiskSerializer.php', 1719 1720 'PhabricatorLocalDiskFileStorageEngine' => 'applications/files/engine/PhabricatorLocalDiskFileStorageEngine.php', 1720 1721 'PhabricatorLocalTimeTestCase' => 'view/__tests__/PhabricatorLocalTimeTestCase.php', 1721 1722 'PhabricatorLogoutController' => 'applications/auth/controller/PhabricatorLogoutController.php', ··· 2561 2562 'PhortuneController' => 'applications/phortune/controller/PhortuneController.php', 2562 2563 'PhortuneCreditCardForm' => 'applications/phortune/view/PhortuneCreditCardForm.php', 2563 2564 'PhortuneCurrency' => 'applications/phortune/currency/PhortuneCurrency.php', 2565 + 'PhortuneCurrencySerializer' => 'applications/phortune/currency/PhortuneCurrencySerializer.php', 2564 2566 'PhortuneCurrencyTestCase' => 'applications/phortune/currency/__tests__/PhortuneCurrencyTestCase.php', 2565 2567 'PhortuneDAO' => 'applications/phortune/storage/PhortuneDAO.php', 2566 2568 'PhortuneErrCode' => 'applications/phortune/constants/PhortuneErrCode.php', ··· 5591 5593 'PhortuneChargeQuery' => 'PhabricatorCursorPagedPolicyAwareQuery', 5592 5594 'PhortuneController' => 'PhabricatorController', 5593 5595 'PhortuneCurrency' => 'Phobject', 5596 + 'PhortuneCurrencySerializer' => 'PhabricatorLiskSerializer', 5594 5597 'PhortuneCurrencyTestCase' => 'PhabricatorTestCase', 5595 5598 'PhortuneDAO' => 'PhabricatorLiskDAO', 5596 5599 'PhortuneErrCode' => 'PhortuneConstants',
+1 -1
src/applications/fund/controller/FundInitiativeBackController.php
··· 57 57 $backer = FundBacker::initializeNewBacker($viewer) 58 58 ->setInitiativePHID($initiative->getPHID()) 59 59 ->attachInitiative($initiative) 60 - ->setAmountInCents($currency->getValue()) 60 + ->setAmountAsCurrency($currency) 61 61 ->save(); 62 62 63 63 // TODO: Here, we'd create a purchase and cart.
+1 -2
src/applications/fund/query/FundBackerSearchEngine.php
··· 128 128 foreach ($backers as $backer) { 129 129 $backer_handle = $handles[$backer->getBackerPHID()]; 130 130 131 - $currency = PhortuneCurrency::newFromUSDCents( 132 - $backer->getAmountInCents()); 131 + $currency = $backer->getAmount(); 133 132 134 133 $header = pht( 135 134 '%s for %s',
+5 -7
src/applications/fund/storage/FundBacker.php
··· 7 7 8 8 protected $initiativePHID; 9 9 protected $backerPHID; 10 - protected $amountInCents; 10 + protected $amountAsCurrency; 11 11 protected $status; 12 12 protected $properties = array(); 13 13 ··· 28 28 self::CONFIG_SERIALIZATION => array( 29 29 'properties' => self::SERIALIZATION_JSON, 30 30 ), 31 + self::CONFIG_APPLICATION_SERIALIZERS => array( 32 + 'amountAsCurrency' => new PhortuneCurrencySerializer(), 33 + ), 31 34 self::CONFIG_COLUMN_SCHEMA => array( 32 35 'status' => 'text32', 33 - 'amountInCents' => 'uint32', 36 + 'amountAsCurrency' => 'text64', 34 37 ), 35 38 self::CONFIG_KEY_SCHEMA => array( 36 39 'key_initiative' => array( ··· 45 48 46 49 public function generatePHID() { 47 50 return PhabricatorPHID::generateNewPHID(FundBackerPHIDType::TYPECONST); 48 - } 49 - 50 - protected function didReadData() { 51 - // The payment processing code is strict about types. 52 - $this->amountInCents = (int)$this->amountInCents; 53 51 } 54 52 55 53 public function getProperty($key, $default = null) {
+17
src/applications/harbormaster/storage/HarbormasterSchemaSpec.php
··· 5 5 public function buildSchemata() { 6 6 $this->buildEdgeSchemata(new HarbormasterBuildable()); 7 7 8 + // NOTE: This table is not used by any Harbormaster objects, but is used 9 + // by unit tests. 10 + $this->buildRawSchema( 11 + id(new HarbormasterObject())->getApplicationName(), 12 + PhabricatorLiskDAO::COUNTER_TABLE_NAME, 13 + array( 14 + 'counterName' => 'text32', 15 + 'counterValue' => 'id64', 16 + ), 17 + array( 18 + 'PRIMARY' => array( 19 + 'columns' => array('counterName'), 20 + 'unique' => true, 21 + ), 22 + )); 23 + 24 + 8 25 $this->buildRawSchema( 9 26 id(new HarbormasterBuildable())->getApplicationName(), 10 27 'harbormaster_buildlogchunk',
+2 -3
src/applications/phortune/controller/PhortuneAccountViewController.php
··· 51 51 ->setObject($account) 52 52 ->setUser($user); 53 53 54 - $properties->addProperty(pht('Balance'), $account->getBalanceInCents()); 54 + $properties->addProperty(pht('Balance'), '-'); 55 55 $properties->setActionList($actions); 56 56 57 57 $payment_methods = $this->buildPaymentMethodsSection($account); ··· 189 189 foreach ($cart->getPurchases() as $purchase) { 190 190 $id = $purchase->getID(); 191 191 192 - $price = $purchase->getTotalPriceInCents(); 193 - $price = PhortuneCurrency::newFromUSDCents($price)->formatForDisplay(); 192 + $price = $purchase->getTotalPriceAsCurrency()->formatForDisplay(); 194 193 195 194 $purchase_link = phutil_tag( 196 195 'a',
+1 -1
src/applications/phortune/controller/PhortuneCartCheckoutController.php
··· 59 59 ->setAuthorPHID($viewer->getPHID()) 60 60 ->setPaymentProviderKey($provider->getProviderKey()) 61 61 ->setPaymentMethodPHID($method->getPHID()) 62 - ->setAmountInCents($cart->getTotalPriceInCents()) 62 + ->setAmountAsCurrency($cart->getTotalPriceAsCurrency()) 63 63 ->setStatus(PhortuneCharge::STATUS_PENDING); 64 64 65 65 $charge->openTransaction();
+3 -8
src/applications/phortune/controller/PhortuneCartController.php
··· 6 6 protected function buildCartContents(PhortuneCart $cart) { 7 7 8 8 $rows = array(); 9 - $total = 0; 10 9 foreach ($cart->getPurchases() as $purchase) { 11 10 $rows[] = array( 12 11 $purchase->getFullDisplayName(), 13 - PhortuneCurrency::newFromUSDCents($purchase->getBasePriceInCents()) 14 - ->formatForDisplay(), 12 + $purchase->getBasePriceAsCurrency()->formatForDisplay(), 15 13 $purchase->getQuantity(), 16 - PhortuneCurrency::newFromUSDCents($purchase->getTotalPriceInCents()) 17 - ->formatForDisplay(), 14 + $purchase->getTotalPriceAsCurrency()->formatForDisplay(), 18 15 ); 19 - 20 - $total += $purchase->getTotalPriceInCents(); 21 16 } 22 17 23 18 $rows[] = array( ··· 25 20 '', 26 21 '', 27 22 phutil_tag('strong', array(), 28 - PhortuneCurrency::newFromUSDCents($total)->formatForDisplay()), 23 + $cart->getTotalPriceAsCurrency()->formatForDisplay()), 29 24 ); 30 25 31 26 $table = new AphrontTableView($rows);
+1 -2
src/applications/phortune/controller/PhortuneController.php
··· 73 73 $cart_href, 74 74 $charge->getPaymentProviderKey(), 75 75 $charge->getPaymentMethodPHID(), 76 - PhortuneCurrency::newFromUSDCents($charge->getAmountInCents()) 77 - ->formatForDisplay(), 76 + $charge->getAmountAsCurrency()->formatForDisplay(), 78 77 $charge->getStatus(), 79 78 phabricator_datetime($charge->getDateCreated(), $viewer), 80 79 );
+4 -30
src/applications/phortune/controller/PhortuneProductEditController.php
··· 25 25 $cancel_uri = $this->getApplicationURI( 26 26 'product/view/'.$this->productID.'/'); 27 27 } else { 28 - $product = new PhortuneProduct(); 28 + $product = PhortuneProduct::initializeNewProduct(); 29 29 $is_create = true; 30 30 $cancel_uri = $this->getApplicationURI('product/'); 31 31 } 32 32 33 33 $v_name = $product->getProductName(); 34 - $v_type = $product->getProductType(); 35 - $v_price = (int)$product->getPriceInCents(); 36 - $display_price = PhortuneCurrency::newFromUSDCents($v_price) 37 - ->formatForDisplay(); 34 + $v_price = $product->getPriceAsCurrency()->formatForDisplay(); 35 + $display_price = $v_price; 38 36 39 37 $e_name = true; 40 - $e_type = null; 41 38 $e_price = true; 42 39 $errors = array(); 43 40 ··· 50 47 $e_name = null; 51 48 } 52 49 53 - if ($is_create) { 54 - $v_type = $request->getStr('type'); 55 - $type_map = PhortuneProduct::getTypeMap(); 56 - if (empty($type_map[$v_type])) { 57 - $e_type = pht('Invalid'); 58 - $errors[] = pht('Product type is invalid.'); 59 - } else { 60 - $e_type = null; 61 - } 62 - } 63 - 64 50 $display_price = $request->getStr('price'); 65 51 try { 66 52 $v_price = PhortuneCurrency::newFromUserInput($user, $display_price) 67 - ->getValue(); 53 + ->serializeForStorage(); 68 54 $e_price = null; 69 55 } catch (Exception $ex) { 70 56 $errors[] = pht('Price should be formatted as: $1.23'); ··· 77 63 $xactions[] = id(new PhortuneProductTransaction()) 78 64 ->setTransactionType(PhortuneProductTransaction::TYPE_NAME) 79 65 ->setNewValue($v_name); 80 - 81 - $xactions[] = id(new PhortuneProductTransaction()) 82 - ->setTransactionType(PhortuneProductTransaction::TYPE_TYPE) 83 - ->setNewValue($v_type); 84 66 85 67 $xactions[] = id(new PhortuneProductTransaction()) 86 68 ->setTransactionType(PhortuneProductTransaction::TYPE_PRICE) ··· 111 93 ->setName('name') 112 94 ->setValue($v_name) 113 95 ->setError($e_name)) 114 - ->appendChild( 115 - id(new AphrontFormSelectControl()) 116 - ->setLabel(pht('Type')) 117 - ->setName('type') 118 - ->setValue($v_type) 119 - ->setError($e_type) 120 - ->setOptions(PhortuneProduct::getTypeMap()) 121 - ->setDisabled(!$is_create)) 122 96 ->appendChild( 123 97 id(new AphrontFormTextControl()) 124 98 ->setLabel(pht('Price'))
+2 -4
src/applications/phortune/controller/PhortuneProductListController.php
··· 32 32 $view_uri = $this->getApplicationURI( 33 33 'product/view/'.$product->getID().'/'); 34 34 35 - $price = $product->getPriceInCents(); 35 + $price = $product->getPriceAsCurrency(); 36 36 37 37 $item = id(new PHUIObjectItemView()) 38 38 ->setObjectName($product->getID()) 39 39 ->setHeader($product->getProductName()) 40 40 ->setHref($view_uri) 41 - ->addAttribute( 42 - PhortuneCurrency::newFromUSDCents($price)->formatForDisplay()) 43 - ->addAttribute($product->getTypeName()); 41 + ->addAttribute($price->formatForDisplay()); 44 42 45 43 $product_list->addItem($item); 46 44 }
+2 -3
src/applications/phortune/controller/PhortuneProductPurchaseController.php
··· 49 49 $purchase->setAccountPHID($account->getPHID()); 50 50 $purchase->setAuthorPHID($user->getPHID()); 51 51 $purchase->setCartPHID($cart->getPHID()); 52 - $purchase->setBasePriceInCents($product->getPriceInCents()); 52 + $purchase->setBasePriceAsCurrency($product->getPriceAsCurrency()); 53 53 $purchase->setQuantity(1); 54 - $purchase->setTotalPriceInCents( 55 - $purchase->getBasePriceInCents() * $purchase->getQuantity()); 54 + 56 55 $purchase->setStatus(PhortunePurchase::STATUS_PENDING); 57 56 $purchase->save(); 58 57
+1 -3
src/applications/phortune/controller/PhortuneProductViewController.php
··· 60 60 $properties = id(new PHUIPropertyListView()) 61 61 ->setUser($user) 62 62 ->setActionList($actions) 63 - ->addProperty(pht('Type'), $product->getTypeName()) 64 63 ->addProperty( 65 64 pht('Price'), 66 - PhortuneCurrency::newFromUSDCents($product->getPriceInCents()) 67 - ->formatForDisplay()); 65 + $product->getPriceAsCurrency()->formatForDisplay()); 68 66 69 67 $xactions = id(new PhortuneProductTransactionQuery()) 70 68 ->setViewer($user)
+41 -19
src/applications/phortune/currency/PhortuneCurrency.php
··· 9 9 // Intentionally private. 10 10 } 11 11 12 + public static function getDefaultCurrency() { 13 + return 'USD'; 14 + } 15 + 16 + public static function newEmptyCurrency() { 17 + return self::newFromString('0.00 USD'); 18 + } 19 + 12 20 public static function newFromUserInput(PhabricatorUser $user, $string) { 21 + // Eventually, this might select a default currency based on user settings. 22 + return self::newFromString($string, self::getDefaultCurrency()); 23 + } 24 + 25 + public static function newFromString($string, $default = null) { 13 26 $matches = null; 14 27 $ok = preg_match( 15 28 '/^([-$]*(?:\d+)?(?:[.]\d{0,2})?)(?:\s+([A-Z]+))?$/', ··· 34 47 $value = (float)$value; 35 48 $value = (int)round(100 * $value); 36 49 37 - $currency = idx($matches, 2, 'USD'); 50 + $currency = idx($matches, 2, $default); 38 51 if ($currency) { 39 52 switch ($currency) { 40 53 case 'USD': ··· 44 57 } 45 58 } 46 59 60 + return self::newFromValueAndCurrency($value, $currency); 61 + } 62 + 63 + public static function newFromValueAndCurrency($value, $currency) { 47 64 $obj = new PhortuneCurrency(); 48 65 49 66 $obj->value = $value; ··· 56 73 assert_instances_of($list, 'PhortuneCurrency'); 57 74 58 75 $total = 0; 76 + $currency = null; 59 77 foreach ($list as $item) { 78 + if ($currency === null) { 79 + $currency = $item->getCurrency(); 80 + } else if ($currency === $item->getCurrency()) { 81 + // Adding a value denominated in the same currency, which is 82 + // fine. 83 + } else { 84 + throw new Exception( 85 + pht('Trying to sum a list of unlike currencies.')); 86 + } 87 + 60 88 // TODO: This should check for integer overflows, etc. 61 89 $total += $item->getValue(); 62 90 } 63 91 64 - return PhortuneCurrency::newFromUSDCents($total); 65 - } 66 - 67 - public static function newFromUSDCents($cents) { 68 - if (!is_int($cents)) { 69 - throw new Exception( 70 - pht('USDCents value "%s" is not an integer!', $cents)); 71 - } 72 - 73 - $obj = new PhortuneCurrency(); 74 - 75 - $obj->value = $cents; 76 - $obj->currency = 'USD'; 77 - 78 - return $obj; 92 + return PhortuneCurrency::newFromValueAndCurrency( 93 + $total, 94 + self::getDefaultCurrency()); 79 95 } 80 96 81 97 public function formatForDisplay() { 82 98 $bare = $this->formatBareValue(); 83 - return '$'.$bare.' USD'; 99 + return '$'.$bare.' '.$this->currency; 100 + } 101 + 102 + public function serializeForStorage() { 103 + return $this->formatBareValue().' '.$this->currency; 84 104 } 85 105 86 106 public function formatBareValue() { ··· 88 108 case 'USD': 89 109 return sprintf('%.02f', $this->value / 100); 90 110 default: 91 - throw new Exception('Unsupported currency!'); 92 - 111 + throw new Exception( 112 + pht('Unsupported currency ("%s")!', $this->currency)); 93 113 } 94 114 } 95 115 ··· 104 124 private static function throwFormatException($string) { 105 125 throw new Exception("Invalid currency format ('{$string}')."); 106 126 } 127 + 128 + 107 129 108 130 }
+20
src/applications/phortune/currency/PhortuneCurrencySerializer.php
··· 1 + <?php 2 + 3 + final class PhortuneCurrencySerializer extends PhabricatorLiskSerializer { 4 + 5 + public function willReadValue($value) { 6 + return PhortuneCurrency::newFromString($value); 7 + } 8 + 9 + public function willWriteValue($value) { 10 + if (!($value instanceof PhortuneCurrency)) { 11 + throw new Exception( 12 + pht( 13 + 'Trying to save object with a currency column, but the column '. 14 + 'value is not a PhortuneCurrency object.')); 15 + } 16 + 17 + return $value->serializeForStorage(); 18 + } 19 + 20 + }
+19 -23
src/applications/phortune/currency/__tests__/PhortuneCurrencyTestCase.php
··· 4 4 5 5 public function testCurrencyFormatForDisplay() { 6 6 $map = array( 7 - 0 => '$0.00 USD', 8 - 1 => '$0.01 USD', 9 - 100 => '$1.00 USD', 10 - -123 => '$-1.23 USD', 11 - 5000000 => '$50000.00 USD', 7 + '0' => '$0.00 USD', 8 + '.01' => '$0.01 USD', 9 + '1.00' => '$1.00 USD', 10 + '-1.23' => '$-1.23 USD', 11 + '50000.00' => '$50000.00 USD', 12 12 ); 13 13 14 14 foreach ($map as $input => $expect) { 15 15 $this->assertEqual( 16 16 $expect, 17 - PhortuneCurrency::newFromUSDCents($input)->formatForDisplay(), 18 - "formatForDisplay({$input})"); 17 + PhortuneCurrency::newFromString($input, 'USD')->formatForDisplay(), 18 + "newFromString({$input})->formatForDisplay()"); 19 19 } 20 20 } 21 21 ··· 25 25 // NOTE: The PayPal API depends on the behavior of the bare value format! 26 26 27 27 $map = array( 28 - 0 => '0.00', 29 - 1 => '0.01', 30 - 100 => '1.00', 31 - -123 => '-1.23', 32 - 5000000 => '50000.00', 28 + '0' => '0.00', 29 + '.01' => '0.01', 30 + '1.00' => '1.00', 31 + '-1.23' => '-1.23', 32 + '50000.00' => '50000.00', 33 33 ); 34 34 35 35 foreach ($map as $input => $expect) { 36 36 $this->assertEqual( 37 37 $expect, 38 - PhortuneCurrency::newFromUSDCents($input)->formatBareValue(), 39 - "formatBareValue({$input})"); 38 + PhortuneCurrency::newFromString($input, 'USD')->formatBareValue(), 39 + "newFromString({$input})->formatBareValue()"); 40 40 } 41 41 } 42 42 43 - public function testCurrencyFromUserInput() { 43 + public function testCurrencyFromString() { 44 44 45 45 $map = array( 46 46 '1.00' => 100, ··· 56 56 '$-.99' => -99, 57 57 '$.99 USD' => 99, 58 58 ); 59 - 60 - $user = new PhabricatorUser(); 61 59 62 60 foreach ($map as $input => $expect) { 63 61 $this->assertEqual( 64 62 $expect, 65 - PhortuneCurrency::newFromUserInput($user, $input)->getValue(), 66 - "newFromUserInput({$input})->getValue()"); 63 + PhortuneCurrency::newFromString($input, 'USD')->getValue(), 64 + "newFromString({$input})->getValue()"); 67 65 } 68 66 } 69 67 70 - public function testInvalidCurrencyFromUserInput() { 68 + public function testInvalidCurrencyFromString() { 71 69 $map = array( 72 70 '--1', 73 71 '$$1', ··· 77 75 '1 dollar', 78 76 ); 79 77 80 - $user = new PhabricatorUser(); 81 - 82 78 foreach ($map as $input) { 83 79 $caught = null; 84 80 try { 85 - PhortuneCurrency::newFromUserInput($user, $input); 81 + PhortuneCurrency::newFromString($input, 'USD'); 86 82 } catch (Exception $ex) { 87 83 $caught = $ex; 88 84 }
+3 -10
src/applications/phortune/editor/PhortuneProductEditor.php
··· 16 16 $types = parent::getTransactionTypes(); 17 17 18 18 $types[] = PhortuneProductTransaction::TYPE_NAME; 19 - $types[] = PhortuneProductTransaction::TYPE_TYPE; 20 19 $types[] = PhortuneProductTransaction::TYPE_PRICE; 21 20 22 21 return $types; ··· 29 28 switch ($xaction->getTransactionType()) { 30 29 case PhortuneProductTransaction::TYPE_NAME: 31 30 return $object->getProductName(); 32 - case PhortuneProductTransaction::TYPE_TYPE: 33 - return $object->getProductType(); 34 31 case PhortuneProductTransaction::TYPE_PRICE: 35 - return $object->getPriceInCents(); 32 + return $object->getPriceAsCurrency()->serializeForStorage(); 36 33 } 37 34 return parent::getCustomTransactionOldValue($object, $xaction); 38 35 } ··· 42 39 PhabricatorApplicationTransaction $xaction) { 43 40 switch ($xaction->getTransactionType()) { 44 41 case PhortuneProductTransaction::TYPE_NAME: 45 - case PhortuneProductTransaction::TYPE_TYPE: 46 42 case PhortuneProductTransaction::TYPE_PRICE: 47 43 return $xaction->getNewValue(); 48 44 } ··· 56 52 case PhortuneProductTransaction::TYPE_NAME: 57 53 $object->setProductName($xaction->getNewValue()); 58 54 return; 59 - case PhortuneProductTransaction::TYPE_TYPE: 60 - $object->setProductType($xaction->getNewValue()); 61 - return; 62 55 case PhortuneProductTransaction::TYPE_PRICE: 63 - $object->setPriceInCents($xaction->getNewValue()); 56 + $object->setPriceAsCurrency( 57 + PhortuneCurrency::newFromString($xaction->getNewValue())); 64 58 return; 65 59 } 66 60 return parent::applyCustomInternalTransaction($object, $xaction); ··· 71 65 PhabricatorApplicationTransaction $xaction) { 72 66 switch ($xaction->getTransactionType()) { 73 67 case PhortuneProductTransaction::TYPE_NAME: 74 - case PhortuneProductTransaction::TYPE_TYPE: 75 68 case PhortuneProductTransaction::TYPE_PRICE: 76 69 return; 77 70 }
+1 -2
src/applications/phortune/provider/PhortunePaypalPaymentProvider.php
··· 93 93 'cartID' => $cart->getID(), 94 94 )); 95 95 96 - $total_in_cents = $cart->getTotalPriceInCents(); 97 - $price = PhortuneCurrency::newFromUSDCents($total_in_cents); 96 + $price = $cart->getTotalPriceAsCurrency(); 98 97 99 98 $result = $this 100 99 ->newPaypalAPICall()
+4 -2
src/applications/phortune/provider/PhortuneStripePaymentProvider.php
··· 47 47 $root = dirname(phutil_get_library_root('phabricator')); 48 48 require_once $root.'/externals/stripe-php/lib/Stripe.php'; 49 49 50 + $price = $charge->getAmountAsCurrency(); 51 + 50 52 $secret_key = $this->getSecretKey(); 51 53 $params = array( 52 - 'amount' => $charge->getAmountInCents(), 53 - 'currency' => 'usd', 54 + 'amount' => $price->getValue(), 55 + 'currency' => $price->getCurrency(), 54 56 'customer' => $method->getMetadataValue('stripe.customerID'), 55 57 'description' => $charge->getPHID(), 56 58 'capture' => true,
+4 -3
src/applications/phortune/provider/PhortuneWePayPaymentProvider.php
··· 116 116 'cartID' => $cart->getID(), 117 117 )); 118 118 119 - $total_in_cents = $cart->getTotalPriceInCents(); 120 - $price = PhortuneCurrency::newFromUSDCents($total_in_cents); 119 + $price = $cart->getTotalPriceAsCurrency(); 121 120 122 121 $params = array( 123 122 'account_id' => $this->getWePayAccountID(), ··· 176 175 $result->state)); 177 176 } 178 177 178 + $currency = PhortuneCurrency::newFromString($checkout->gross, 'USD'); 179 + 179 180 $unguarded = AphrontWriteGuard::beginScopedUnguardedWrites(); 180 181 181 182 $charge = id(new PhortuneCharge()) 182 - ->setAmountInCents((int)$checkout->gross * 100) 183 + ->setAmountAsCurrency($currency) 183 184 ->setAccountPHID($cart->getAccount()->getPHID()) 184 185 ->setAuthorPHID($viewer->getPHID()) 185 186 ->setPaymentProviderKey($this->getProviderKey())
-2
src/applications/phortune/storage/PhortuneAccount.php
··· 10 10 implements PhabricatorPolicyInterface { 11 11 12 12 protected $name; 13 - protected $balanceInCents = 0; 14 13 15 14 private $memberPHIDs = self::ATTACHABLE; 16 15 ··· 19 18 self::CONFIG_AUX_PHID => true, 20 19 self::CONFIG_COLUMN_SCHEMA => array( 21 20 'name' => 'text255', 22 - 'balanceInCents' => 'sint64', 23 21 ), 24 22 ) + parent::getConfiguration(); 25 23 }
+3 -4
src/applications/phortune/storage/PhortuneCart.php
··· 56 56 return $this->assertAttached($this->account); 57 57 } 58 58 59 - public function getTotalPriceInCents() { 59 + public function getTotalPriceAsCurrency() { 60 60 $prices = array(); 61 61 foreach ($this->getPurchases() as $purchase) { 62 - $prices[] = PhortuneCurrency::newFromUSDCents( 63 - $purchase->getTotalPriceInCents()); 62 + $prices[] = $purchase->getTotalPriceAsCurrency(); 64 63 } 65 64 66 - return PhortuneCurrency::newFromList($prices)->getValue(); 65 + return PhortuneCurrency::newFromList($prices); 67 66 } 68 67 69 68
+5 -7
src/applications/phortune/storage/PhortuneCharge.php
··· 20 20 protected $cartPHID; 21 21 protected $paymentProviderKey; 22 22 protected $paymentMethodPHID; 23 - protected $amountInCents; 23 + protected $amountAsCurrency; 24 24 protected $status; 25 25 protected $metadata = array(); 26 26 ··· 33 33 self::CONFIG_SERIALIZATION => array( 34 34 'metadata' => self::SERIALIZATION_JSON, 35 35 ), 36 + self::CONFIG_APPLICATION_SERIALIZERS => array( 37 + 'amountAsCurrency' => new PhortuneCurrencySerializer(), 38 + ), 36 39 self::CONFIG_COLUMN_SCHEMA => array( 37 40 'paymentProviderKey' => 'text128', 38 41 'paymentMethodPHID' => 'phid?', 39 - 'amountInCents' => 'sint32', 42 + 'amountAsCurrency' => 'text64', 40 43 'status' => 'text32', 41 44 ), 42 45 self::CONFIG_KEY_SCHEMA => array( ··· 53 56 public function generatePHID() { 54 57 return PhabricatorPHID::generateNewPHID( 55 58 PhabricatorPHIDConstants::PHID_TYPE_CHRG); 56 - } 57 - 58 - protected function didReadData() { 59 - // The payment processing code is strict about types. 60 - $this->amountInCents = (int)$this->amountInCents; 61 59 } 62 60 63 61 public function getMetadataValue($key, $default = null) {
+9 -38
src/applications/phortune/storage/PhortuneProduct.php
··· 1 1 <?php 2 2 3 3 /** 4 - * A product is something users can purchase. It may be a one-time purchase, 5 - * or a plan which is billed monthly. 4 + * A product is something users can purchase. 6 5 */ 7 6 final class PhortuneProduct extends PhortuneDAO 8 7 implements PhabricatorPolicyInterface { 9 8 10 - const TYPE_BILL_ONCE = 'phortune:thing'; 11 - const TYPE_BILL_PLAN = 'phortune:plan'; 12 - 13 - const STATUS_ACTIVE = 'product:active'; 14 - const STATUS_DISABLED = 'product:disabled'; 15 - 16 9 protected $productName; 17 - protected $productType; 18 - protected $status = self::STATUS_ACTIVE; 19 - protected $priceInCents; 20 - protected $billingIntervalInMonths; 21 - protected $trialPeriodInDays; 10 + protected $priceAsCurrency; 22 11 protected $metadata; 23 12 24 13 public function getConfiguration() { ··· 26 15 self::CONFIG_AUX_PHID => true, 27 16 self::CONFIG_SERIALIZATION => array( 28 17 'metadata' => self::SERIALIZATION_JSON, 18 + ), 19 + self::CONFIG_APPLICATION_SERIALIZERS => array( 20 + 'priceAsCurrency' => new PhortuneCurrencySerializer(), 29 21 ), 30 22 self::CONFIG_COLUMN_SCHEMA => array( 31 23 'productName' => 'text255', 32 - 'productType' => 'text64', 33 24 'status' => 'text64', 34 - 'priceInCents' => 'sint64', 25 + 'priceAsCurrency' => 'text64', 35 26 'billingIntervalInMonths' => 'uint32?', 36 27 'trialPeriodInDays' => 'uint32?', 37 28 ), 38 - self::CONFIG_KEY_SCHEMA => array( 39 - 'key_status' => array( 40 - 'columns' => array('status'), 41 - ), 42 - ), 43 29 ) + parent::getConfiguration(); 44 30 } 45 31 ··· 48 34 PhabricatorPHIDConstants::PHID_TYPE_PDCT); 49 35 } 50 36 51 - public static function getTypeMap() { 52 - return array( 53 - self::TYPE_BILL_ONCE => pht('Product (Charged Once)'), 54 - self::TYPE_BILL_PLAN => pht('Flat Rate Plan (Charged Monthly)'), 55 - ); 56 - } 57 - 58 - public function getTypeName() { 59 - return idx(self::getTypeMap(), $this->getProductType()); 60 - } 61 - 62 - public function getPriceInCents() { 63 - $price = parent::getPriceInCents(); 64 - if ($price === null) { 65 - return $price; 66 - } else { 67 - return (int)parent::getPriceInCents(); 68 - } 37 + public static function initializeNewProduct() { 38 + return id(new PhortuneProduct()) 39 + ->setPriceAsCurrency(PhortuneCurrency::newEmptyCurrency()); 69 40 } 70 41 71 42
+3 -19
src/applications/phortune/storage/PhortuneProductTransaction.php
··· 4 4 extends PhabricatorApplicationTransaction { 5 5 6 6 const TYPE_NAME = 'product:name'; 7 - const TYPE_TYPE = 'product:type'; 8 7 const TYPE_PRICE = 'product:price'; 9 8 10 9 public function getApplicationName() { ··· 44 43 return pht( 45 44 '%s set product price to %s.', 46 45 $this->renderHandleLink($author_phid), 47 - PhortuneCurrency::newFromUSDCents($new) 46 + PhortuneCurrency::newFromString($new) 48 47 ->formatForDisplay()); 49 48 } else { 50 49 return pht( 51 50 '%s changed product price from %s to %s.', 52 51 $this->renderHandleLink($author_phid), 53 - PhortuneCurrency::newFromUSDCents($old) 52 + PhortuneCurrency::newFromString($old) 54 53 ->formatForDisplay(), 55 - PhortuneCurrency::newFromUSDCents($new) 54 + PhortuneCurrency::newFromString($new) 56 55 ->formatForDisplay()); 57 - } 58 - break; 59 - case self::TYPE_TYPE: 60 - $map = PhortuneProduct::getTypeMap(); 61 - if ($old === null) { 62 - return pht( 63 - '%s set product type to "%s".', 64 - $this->renderHandleLink($author_phid), 65 - $map[$new]); 66 - } else { 67 - return pht( 68 - '%s changed product type from "%s" to "%s".', 69 - $this->renderHandleLink($author_phid), 70 - $map[$old], 71 - $map[$new]); 72 56 } 73 57 break; 74 58 }
+9 -10
src/applications/phortune/storage/PhortunePurchase.php
··· 17 17 protected $accountPHID; 18 18 protected $authorPHID; 19 19 protected $cartPHID; 20 - protected $basePriceInCents; 20 + protected $basePriceAsCurrency; 21 21 protected $quantity; 22 - protected $totalPriceInCents; 23 22 protected $status; 24 23 protected $metadata; 25 24 ··· 31 30 self::CONFIG_SERIALIZATION => array( 32 31 'metadata' => self::SERIALIZATION_JSON, 33 32 ), 33 + self::CONFIG_APPLICATION_SERIALIZERS => array( 34 + 'basePriceAsCurrency' => new PhortuneCurrencySerializer(), 35 + ), 34 36 self::CONFIG_COLUMN_SCHEMA => array( 35 37 'cartPHID' => 'phid?', 36 - 'basePriceInCents' => 'sint32', 38 + 'basePriceAsCurrency' => 'text64', 37 39 'quantity' => 'uint32', 38 - 'totalPriceInCents' => 'sint32', 39 40 'status' => 'text32', 40 41 ), 41 42 self::CONFIG_KEY_SCHEMA => array( ··· 60 61 return $this->assertAttached($this->cart); 61 62 } 62 63 63 - protected function didReadData() { 64 - // The payment processing code is strict about types. 65 - $this->basePriceInCents = (int)$this->basePriceInCents; 66 - $this->totalPriceInCents = (int)$this->totalPriceInCents; 64 + public function getFullDisplayName() { 65 + return pht('Goods and/or Services'); 67 66 } 68 67 69 - public function getFullDisplayName() { 70 - return pht('Goods and/or Services'); 68 + public function getTotalPriceAsCurrency() { 69 + return $this->getBasePriceAsCurrency(); 71 70 } 72 71 73 72
+28 -6
src/infrastructure/storage/lisk/PhabricatorLiskDAO.php
··· 8 8 private static $namespaceStack = array(); 9 9 10 10 const ATTACHABLE = '<attachable>'; 11 + const CONFIG_APPLICATION_SERIALIZERS = 'phabricator/serializers'; 11 12 12 13 /* -( Configuring Storage )------------------------------------------------ */ 13 14 ··· 209 210 return phutil_utf8ize($string); 210 211 } 211 212 212 - public function delete() { 213 + protected function willReadData(array &$data) { 214 + parent::willReadData($data); 215 + 216 + static $custom; 217 + if ($custom === null) { 218 + $custom = $this->getConfigOption(self::CONFIG_APPLICATION_SERIALIZERS); 219 + } 220 + 221 + if ($custom) { 222 + foreach ($custom as $key => $serializer) { 223 + $data[$key] = $serializer->willReadValue($data[$key]); 224 + } 225 + } 226 + } 227 + 228 + protected function willWriteData(array &$data) { 229 + static $custom; 230 + if ($custom === null) { 231 + $custom = $this->getConfigOption(self::CONFIG_APPLICATION_SERIALIZERS); 232 + } 213 233 214 - // TODO: We should make some reasonable effort to destroy related 215 - // infrastructure objects here, like edges, transactions, custom field 216 - // storage, flags, Phrequent tracking, tokens, etc. This doesn't need to 217 - // be exhaustive, but we can get a lot of it pretty easily. 234 + if ($custom) { 235 + foreach ($custom as $key => $serializer) { 236 + $data[$key] = $serializer->willWriteValue($data[$key]); 237 + } 238 + } 218 239 219 - return parent::delete(); 240 + parent::willWriteData($data); 220 241 } 242 + 221 243 222 244 }
+8
src/infrastructure/storage/lisk/PhabricatorLiskSerializer.php
··· 1 + <?php 2 + 3 + abstract class PhabricatorLiskSerializer { 4 + 5 + abstract public function willReadValue($value); 6 + abstract public function willWriteValue($value); 7 + 8 + }