Page MenuHomestyx hydra

No OneTemporary

diff --git a/src/applications/phortune/currency/PhortuneCurrency.php b/src/applications/phortune/currency/PhortuneCurrency.php
index fd9ea08f0e..88b9833e4d 100644
--- a/src/applications/phortune/currency/PhortuneCurrency.php
+++ b/src/applications/phortune/currency/PhortuneCurrency.php
@@ -1,178 +1,185 @@
<?php
final class PhortuneCurrency extends Phobject {
private $value;
private $currency;
private function __construct() {
// 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]+))?$/',
trim($string),
$matches);
if (!$ok) {
self::throwFormatException($string);
}
$value = $matches[1];
if (substr_count($value, '-') > 1) {
self::throwFormatException($string);
}
if (substr_count($value, '$') > 1) {
self::throwFormatException($string);
}
$value = str_replace('$', '', $value);
$value = (float)$value;
$value = (int)round(100 * $value);
$currency = idx($matches, 2, $default);
if ($currency) {
switch ($currency) {
case 'USD':
break;
default:
throw new Exception("Unsupported currency '{$currency}'!");
}
}
return self::newFromValueAndCurrency($value, $currency);
}
public static function newFromValueAndCurrency($value, $currency) {
$obj = new PhortuneCurrency();
$obj->value = $value;
$obj->currency = $currency;
return $obj;
}
public static function newFromList(array $list) {
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::newFromValueAndCurrency(
$total,
self::getDefaultCurrency());
}
public function formatForDisplay() {
$bare = $this->formatBareValue();
return '$'.$bare.' '.$this->currency;
}
public function serializeForStorage() {
return $this->formatBareValue().' '.$this->currency;
}
public function formatBareValue() {
switch ($this->currency) {
case 'USD':
return sprintf('%.02f', $this->value / 100);
default:
throw new Exception(
pht('Unsupported currency ("%s")!', $this->currency));
}
}
public function getValue() {
return $this->value;
}
public function getCurrency() {
return $this->currency;
}
+ public function getValueInUSDCents() {
+ if ($this->currency !== 'USD') {
+ throw new Exception(pht('Unexpected currency!'));
+ }
+ return $this->value;
+ }
+
private static function throwFormatException($string) {
throw new Exception("Invalid currency format ('{$string}').");
}
/**
* Assert that a currency value lies within a range.
*
* Throws if the value is not between the minimum and maximum, inclusive.
*
* In particular, currency values can be negative (to represent a debt or
* credit), so checking against zero may be useful to make sure a value
* has the expected sign.
*
* @param string|null Currency string, or null to skip check.
* @param string|null Currency string, or null to skip check.
* @return this
*/
public function assertInRange($minimum, $maximum) {
if ($minimum !== null && $maximum !== null) {
$min = PhortuneCurrency::newFromString($minimum);
$max = PhortuneCurrency::newFromString($maximum);
if ($min->value > $max->value) {
throw new Exception(
pht(
'Range (%s - %s) is not valid!',
$min->formatForDisplay(),
$max->formatForDisplay()));
}
}
if ($minimum !== null) {
$min = PhortuneCurrency::newFromString($minimum);
if ($min->value > $this->value) {
throw new Exception(
pht(
'Minimum allowed amount is %s.',
$min->formatForDisplay()));
}
}
if ($maximum !== null) {
$max = PhortuneCurrency::newFromString($maximum);
if ($max->value < $this->value) {
throw new Exception(
pht(
'Maximum allowed amount is %s.',
$max->formatForDisplay()));
}
}
return $this;
}
}
diff --git a/src/applications/phortune/provider/PhortuneBalancedPaymentProvider.php b/src/applications/phortune/provider/PhortuneBalancedPaymentProvider.php
index 41d3f3b4d6..a9204a86a1 100644
--- a/src/applications/phortune/provider/PhortuneBalancedPaymentProvider.php
+++ b/src/applications/phortune/provider/PhortuneBalancedPaymentProvider.php
@@ -1,179 +1,215 @@
<?php
final class PhortuneBalancedPaymentProvider extends PhortunePaymentProvider {
public function isEnabled() {
return $this->getMarketplaceURI() &&
$this->getSecretKey();
}
public function getProviderType() {
return 'balanced';
}
public function getProviderDomain() {
return 'balancedpayments.com';
}
public function getPaymentMethodDescription() {
return pht('Add Credit or Debit Card');
}
public function getPaymentMethodIcon() {
return celerity_get_resource_uri('/rsrc/image/phortune/balanced.png');
}
public function getPaymentMethodProviderDescription() {
return pht('Processed by Balanced');
}
public function getDefaultPaymentMethodDisplayName(
PhortunePaymentMethod $method) {
return pht('Credit/Debit Card');
}
public function canHandlePaymentMethod(PhortunePaymentMethod $method) {
$type = $method->getMetadataValue('type');
return ($type === 'balanced.account');
}
protected function executeCharge(
PhortunePaymentMethod $method,
PhortuneCharge $charge) {
- throw new PhortuneNotImplementedException($this);
+
+ $root = dirname(phutil_get_library_root('phabricator'));
+ require_once $root.'/externals/httpful/bootstrap.php';
+ require_once $root.'/externals/restful/bootstrap.php';
+ require_once $root.'/externals/balanced-php/bootstrap.php';
+
+ $price = $charge->getAmountAsCurrency();
+
+ // Build the string which will appear on the credit card statement.
+ $charge_as = new PhutilURI(PhabricatorEnv::getProductionURI('/'));
+ $charge_as = $charge_as->getDomain();
+ $charge_as = id(new PhutilUTF8StringTruncator())
+ ->setMaximumBytes(22)
+ ->setTerminator('')
+ ->truncateString($charge_as);
+
+ try {
+ Balanced\Settings::$api_key = $this->getSecretKey();
+ $card = Balanced\Card::get($method->getMetadataValue('balanced.cardURI'));
+ $debit = $card->debit($price->getValueInUSDCents(), $charge_as);
+ } catch (RESTful\Exceptions\HTTPError $error) {
+ // NOTE: This exception doesn't print anything meaningful if it escapes
+ // to top level. Replace it with something slightly readable.
+ throw new Exception($error->response->body->description);
+ }
+
+ $expect_status = 'succeeded';
+ if ($debit->status !== $expect_status) {
+ throw new Exception(
+ pht(
+ 'Debit failed, expected "%s", got "%s".',
+ $expect_status,
+ $debit->status));
+ }
+
+ $charge->setMetadataValue('balanced.debitURI', $debit->uri);
+ $charge->save();
}
private function getMarketplaceURI() {
return PhabricatorEnv::getEnvConfig('phortune.balanced.marketplace-uri');
}
private function getSecretKey() {
return PhabricatorEnv::getEnvConfig('phortune.balanced.secret-key');
}
/* -( Adding Payment Methods )--------------------------------------------- */
public function canCreatePaymentMethods() {
return true;
}
public function validateCreatePaymentMethodToken(array $token) {
return isset($token['balancedMarketplaceURI']);
}
/**
* @phutil-external-symbol class Balanced\Card
* @phutil-external-symbol class Balanced\Settings
* @phutil-external-symbol class Balanced\Marketplace
* @phutil-external-symbol class RESTful\Exceptions\HTTPError
*/
public function createPaymentMethodFromRequest(
AphrontRequest $request,
PhortunePaymentMethod $method,
array $token) {
$errors = array();
$root = dirname(phutil_get_library_root('phabricator'));
require_once $root.'/externals/httpful/bootstrap.php';
require_once $root.'/externals/restful/bootstrap.php';
require_once $root.'/externals/balanced-php/bootstrap.php';
$account_phid = $method->getAccountPHID();
$author_phid = $method->getAuthorPHID();
$description = $account_phid.':'.$author_phid;
try {
Balanced\Settings::$api_key = $this->getSecretKey();
$card = Balanced\Card::get($token['balancedMarketplaceURI']);
$buyer = Balanced\Marketplace::mine()->createBuyer(
null,
$card->uri,
array(
'description' => $description,
));
} catch (RESTful\Exceptions\HTTPError $error) {
// NOTE: This exception doesn't print anything meaningful if it escapes
// to top level. Replace it with something slightly readable.
throw new Exception($error->response->body->description);
}
$method
->setBrand($card->brand)
->setLastFourDigits($card->last_four)
->setExpires($card->expiration_year, $card->expiration_month)
->setMetadata(
array(
'type' => 'balanced.account',
'balanced.accountURI' => $buyer->uri,
'balanced.cardURI' => $card->uri,
));
return $errors;
}
public function renderCreatePaymentMethodForm(
AphrontRequest $request,
array $errors) {
$ccform = id(new PhortuneCreditCardForm())
->setUser($request->getUser())
->setErrors($errors)
->addScript('https://js.balancedpayments.com/v1/balanced.js');
Javelin::initBehavior(
'balanced-payment-form',
array(
'balancedMarketplaceURI' => $this->getMarketplaceURI(),
'formID' => $ccform->getFormID(),
));
return $ccform->buildForm();
}
private function getBalancedShortErrorCode($error_code) {
$prefix = 'cc:balanced:';
if (strncmp($error_code, $prefix, strlen($prefix))) {
return null;
}
return substr($error_code, strlen($prefix));
}
public function translateCreatePaymentMethodErrorCode($error_code) {
$short_code = $this->getBalancedShortErrorCode($error_code);
if ($short_code) {
static $map = array(
);
if (isset($map[$short_code])) {
return $map[$short_code];
}
}
return $error_code;
}
public function getCreatePaymentMethodErrorMessage($error_code) {
$short_code = $this->getBalancedShortErrorCode($error_code);
if (!$short_code) {
return null;
}
switch ($short_code) {
default:
break;
}
return null;
}
}
diff --git a/src/applications/phortune/provider/PhortuneStripePaymentProvider.php b/src/applications/phortune/provider/PhortuneStripePaymentProvider.php
index 5a19c928d8..957eeb9cb8 100644
--- a/src/applications/phortune/provider/PhortuneStripePaymentProvider.php
+++ b/src/applications/phortune/provider/PhortuneStripePaymentProvider.php
@@ -1,244 +1,244 @@
<?php
final class PhortuneStripePaymentProvider extends PhortunePaymentProvider {
public function isEnabled() {
return $this->getPublishableKey() &&
$this->getSecretKey();
}
public function getProviderType() {
return 'stripe';
}
public function getProviderDomain() {
return 'stripe.com';
}
public function getPaymentMethodDescription() {
return pht('Add Credit or Debit Card (US and Canada)');
}
public function getPaymentMethodIcon() {
return celerity_get_resource_uri('/rsrc/image/phortune/stripe.png');
}
public function getPaymentMethodProviderDescription() {
return pht('Processed by Stripe');
}
public function getDefaultPaymentMethodDisplayName(
PhortunePaymentMethod $method) {
return pht('Credit/Debit Card');
}
public function canHandlePaymentMethod(PhortunePaymentMethod $method) {
$type = $method->getMetadataValue('type');
return ($type === 'stripe.customer');
}
/**
* @phutil-external-symbol class Stripe_Charge
* @phutil-external-symbol class Stripe_CardError
*/
protected function executeCharge(
PhortunePaymentMethod $method,
PhortuneCharge $charge) {
$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' => $price->getValue(),
+ 'amount' => $price->getValueInUSDCents(),
'currency' => $price->getCurrency(),
'customer' => $method->getMetadataValue('stripe.customerID'),
'description' => $charge->getPHID(),
'capture' => true,
);
try {
$stripe_charge = Stripe_Charge::create($params, $secret_key);
} catch (Stripe_CardError $ex) {
// TODO: Fail charge explicitly.
throw $ex;
}
$id = $stripe_charge->id;
if (!$id) {
throw new Exception('Stripe charge call did not return an ID!');
}
$charge->setMetadataValue('stripe.chargeID', $id);
$charge->save();
}
private function getPublishableKey() {
return PhabricatorEnv::getEnvConfig('phortune.stripe.publishable-key');
}
private function getSecretKey() {
return PhabricatorEnv::getEnvConfig('phortune.stripe.secret-key');
}
/* -( Adding Payment Methods )--------------------------------------------- */
public function canCreatePaymentMethods() {
return true;
}
/**
* @phutil-external-symbol class Stripe_Token
* @phutil-external-symbol class Stripe_Customer
*/
public function createPaymentMethodFromRequest(
AphrontRequest $request,
PhortunePaymentMethod $method,
array $token) {
$errors = array();
$root = dirname(phutil_get_library_root('phabricator'));
require_once $root.'/externals/stripe-php/lib/Stripe.php';
$secret_key = $this->getSecretKey();
$stripe_token = $token['stripeCardToken'];
// First, make sure the token is valid.
$info = id(new Stripe_Token())->retrieve($stripe_token, $secret_key);
$account_phid = $method->getAccountPHID();
$author_phid = $method->getAuthorPHID();
$params = array(
'card' => $stripe_token,
'description' => $account_phid.':'.$author_phid,
);
// Then, we need to create a Customer in order to be able to charge
// the card more than once. We create one Customer for each card;
// they do not map to PhortuneAccounts because we allow an account to
// have more than one active card.
$customer = Stripe_Customer::create($params, $secret_key);
$card = $info->card;
$method
->setBrand($card->brand)
->setLastFourDigits($card->last4)
->setExpires($card->exp_year, $card->exp_month)
->setMetadata(
array(
'type' => 'stripe.customer',
'stripe.customerID' => $customer->id,
'stripe.cardToken' => $stripe_token,
));
return $errors;
}
public function renderCreatePaymentMethodForm(
AphrontRequest $request,
array $errors) {
$ccform = id(new PhortuneCreditCardForm())
->setUser($request->getUser())
->setErrors($errors)
->addScript('https://js.stripe.com/v2/');
Javelin::initBehavior(
'stripe-payment-form',
array(
'stripePublishableKey' => $this->getPublishableKey(),
'formID' => $ccform->getFormID(),
));
return $ccform->buildForm();
}
private function getStripeShortErrorCode($error_code) {
$prefix = 'cc:stripe:';
if (strncmp($error_code, $prefix, strlen($prefix))) {
return null;
}
return substr($error_code, strlen($prefix));
}
public function validateCreatePaymentMethodToken(array $token) {
return isset($token['stripeCardToken']);
}
public function translateCreatePaymentMethodErrorCode($error_code) {
$short_code = $this->getStripeShortErrorCode($error_code);
if ($short_code) {
static $map = array(
'error:invalid_number' => PhortuneErrCode::ERR_CC_INVALID_NUMBER,
'error:invalid_cvc' => PhortuneErrCode::ERR_CC_INVALID_CVC,
'error:invalid_expiry_month' => PhortuneErrCode::ERR_CC_INVALID_EXPIRY,
'error:invalid_expiry_year' => PhortuneErrCode::ERR_CC_INVALID_EXPIRY,
);
if (isset($map[$short_code])) {
return $map[$short_code];
}
}
return $error_code;
}
/**
* See https://stripe.com/docs/api#errors for more information on possible
* errors.
*/
public function getCreatePaymentMethodErrorMessage($error_code) {
$short_code = $this->getStripeShortErrorCode($error_code);
if (!$short_code) {
return null;
}
switch ($short_code) {
case 'error:incorrect_number':
$error_key = 'number';
$message = pht('Invalid or incorrect credit card number.');
break;
case 'error:incorrect_cvc':
$error_key = 'cvc';
$message = pht('Card CVC is invalid or incorrect.');
break;
$error_key = 'exp';
$message = pht('Card expiration date is invalid or incorrect.');
break;
case 'error:invalid_expiry_month':
case 'error:invalid_expiry_year':
case 'error:invalid_cvc':
case 'error:invalid_number':
// NOTE: These should be translated into Phortune error codes earlier,
// so we don't expect to receive them here. They are listed for clarity
// and completeness. If we encounter one, we treat it as an unknown
// error.
break;
case 'error:invalid_amount':
case 'error:missing':
case 'error:card_declined':
case 'error:expired_card':
case 'error:duplicate_transaction':
case 'error:processing_error':
default:
// NOTE: These errors currently don't recevive a detailed message.
// NOTE: We can also end up here with "http:nnn" messages.
// TODO: At least some of these should have a better message, or be
// translated into common errors above.
break;
}
return null;
}
}

File Metadata

Mime Type
text/x-diff
Expires
Mon, Jul 28, 10:10 AM (1 w, 1 d ago)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
186694
Default Alt Text
(19 KB)

Event Timeline