1
0
Fork 0
mirror of https://we.phorge.it/source/phorge.git synced 2024-12-28 16:30:59 +01:00

Implement Balanced Payments as a PhortunePaymentProvider

Summary:
Allows Balanced payment methods to be added. This works essentially the same way as Stripe, except everything is a little bit different.

Slightly more stuff could be shared, but I feel //mostly// good about this. I'll probably do a bit more cleanup next. Some of the error handling is messy, in particular.

Ref T2787.

Test Plan: Added Balanced and Stripe payment methods.

Reviewers: btrahan, chad

Reviewed By: btrahan

CC: aran

Maniphest Tasks: T2787

Differential Revision: https://secure.phabricator.com/D5765
This commit is contained in:
epriestley 2013-04-25 09:48:04 -07:00
parent 23786784ef
commit 6efba56448
9 changed files with 308 additions and 35 deletions

View file

@ -1274,6 +1274,20 @@ celerity_register_resource_map(array(
),
'disk' => '/rsrc/js/application/diffusion/behavior-audit-preview.js',
),
'javelin-behavior-balanced-payment-form' =>
array(
'uri' => '/res/2a850a31/rsrc/js/application/phortune/behavior-balanced-payment-form.js',
'type' => 'js',
'requires' =>
array(
0 => 'javelin-behavior',
1 => 'javelin-dom',
2 => 'javelin-json',
3 => 'javelin-workflow',
4 => 'phortune-credit-card-form',
),
'disk' => '/rsrc/js/application/phortune/behavior-balanced-payment-form.js',
),
'javelin-behavior-conpherence-drag-and-drop-photo' =>
array(
'uri' => '/res/9e3eb1cd/rsrc/js/application/conpherence/behavior-drag-and-drop-photo.js',
@ -2258,7 +2272,7 @@ celerity_register_resource_map(array(
),
'javelin-behavior-stripe-payment-form' =>
array(
'uri' => '/res/62dc91b4/rsrc/js/application/phortune/behavior-stripe-payment-form.js',
'uri' => '/res/2ae12d96/rsrc/js/application/phortune/behavior-stripe-payment-form.js',
'type' => 'js',
'requires' =>
array(

View file

@ -1219,6 +1219,7 @@ phutil_register_library_map(array(
'PhabricatorPhabricatorOAuthConfigOptions' => 'applications/config/option/PhabricatorPhabricatorOAuthConfigOptions.php',
'PhabricatorPhameConfigOptions' => 'applications/phame/config/PhabricatorPhameConfigOptions.php',
'PhabricatorPholioConfigOptions' => 'applications/pholio/config/PhabricatorPholioConfigOptions.php',
'PhabricatorPhortuneConfigOptions' => 'applications/phortune/option/PhabricatorPhortuneConfigOptions.php',
'PhabricatorPhrequentConfigOptions' => 'applications/phrequent/config/PhabricatorPhrequentConfigOptions.php',
'PhabricatorPhrictionConfigOptions' => 'applications/phriction/config/PhabricatorPhrictionConfigOptions.php',
'PhabricatorPinboardItemView' => 'view/layout/PhabricatorPinboardItemView.php',
@ -1400,7 +1401,6 @@ phutil_register_library_map(array(
'PhabricatorStorageManagementUpgradeWorkflow' => 'infrastructure/storage/management/workflow/PhabricatorStorageManagementUpgradeWorkflow.php',
'PhabricatorStorageManagementWorkflow' => 'infrastructure/storage/management/workflow/PhabricatorStorageManagementWorkflow.php',
'PhabricatorStoragePatch' => 'infrastructure/storage/management/PhabricatorStoragePatch.php',
'PhabricatorStripeConfigOptions' => 'applications/phortune/option/PhabricatorStripeConfigOptions.php',
'PhabricatorSubscribableInterface' => 'applications/subscriptions/interface/PhabricatorSubscribableInterface.php',
'PhabricatorSubscribersQuery' => 'applications/subscriptions/query/PhabricatorSubscribersQuery.php',
'PhabricatorSubscriptionsEditController' => 'applications/subscriptions/controller/PhabricatorSubscriptionsEditController.php',
@ -1580,6 +1580,7 @@ phutil_register_library_map(array(
'PhortuneAccountTransaction' => 'applications/phortune/storage/PhortuneAccountTransaction.php',
'PhortuneAccountTransactionQuery' => 'applications/phortune/query/PhortuneAccountTransactionQuery.php',
'PhortuneAccountViewController' => 'applications/phortune/controller/PhortuneAccountViewController.php',
'PhortuneBalancedPaymentProvider' => 'applications/phortune/provider/PhortuneBalancedPaymentProvider.php',
'PhortuneCart' => 'applications/phortune/storage/PhortuneCart.php',
'PhortuneCharge' => 'applications/phortune/storage/PhortuneCharge.php',
'PhortuneController' => 'applications/phortune/controller/PhortuneController.php',
@ -2923,6 +2924,7 @@ phutil_register_library_map(array(
'PhabricatorPhabricatorOAuthConfigOptions' => 'PhabricatorApplicationConfigOptions',
'PhabricatorPhameConfigOptions' => 'PhabricatorApplicationConfigOptions',
'PhabricatorPholioConfigOptions' => 'PhabricatorApplicationConfigOptions',
'PhabricatorPhortuneConfigOptions' => 'PhabricatorApplicationConfigOptions',
'PhabricatorPhrequentConfigOptions' => 'PhabricatorApplicationConfigOptions',
'PhabricatorPhrictionConfigOptions' => 'PhabricatorApplicationConfigOptions',
'PhabricatorPinboardItemView' => 'AphrontView',
@ -3091,7 +3093,6 @@ phutil_register_library_map(array(
'PhabricatorStorageManagementStatusWorkflow' => 'PhabricatorStorageManagementWorkflow',
'PhabricatorStorageManagementUpgradeWorkflow' => 'PhabricatorStorageManagementWorkflow',
'PhabricatorStorageManagementWorkflow' => 'PhutilArgumentWorkflow',
'PhabricatorStripeConfigOptions' => 'PhabricatorApplicationConfigOptions',
'PhabricatorSubscribersQuery' => 'PhabricatorQuery',
'PhabricatorSubscriptionsEditController' => 'PhabricatorController',
'PhabricatorSubscriptionsEditor' => 'PhabricatorEditor',
@ -3302,6 +3303,7 @@ phutil_register_library_map(array(
'PhortuneAccountTransaction' => 'PhabricatorApplicationTransaction',
'PhortuneAccountTransactionQuery' => 'PhabricatorApplicationTransactionQuery',
'PhortuneAccountViewController' => 'PhortuneController',
'PhortuneBalancedPaymentProvider' => 'PhortunePaymentProvider',
'PhortuneCart' => 'PhortuneDAO',
'PhortuneCharge' => 'PhortuneDAO',
'PhortuneController' => 'PhabricatorController',

View file

@ -5,7 +5,7 @@ final class PhortuneNotImplementedException extends Exception {
public function __construct(PhortunePaymentProvider $provider) {
$class = get_class($provider);
return parent::__construct(
"Provider '{$provider}' does not implement this method.");
"Provider '{$class}' does not implement this method.");
}
}

View file

@ -0,0 +1,31 @@
<?php
final class PhabricatorPhortuneConfigOptions
extends PhabricatorApplicationConfigOptions {
public function getName() {
return pht("Phortune");
}
public function getDescription() {
return pht("Configure payments and billing.");
}
public function getOptions() {
return array(
$this->newOption('phortune.stripe.publishable-key', 'string', null)
->setLocked(true)
->setDescription(pht('Stripe publishable key.')),
$this->newOption('phortune.stripe.secret-key', 'string', null)
->setHidden(true)
->setDescription(pht('Stripe secret key.')),
$this->newOption('phortune.balanced.marketplace-uri', 'string', null)
->setLocked(true)
->setDescription(pht('Balanced Marketplace URI.')),
$this->newOption('phortune.balanced.secret-key', 'string', null)
->setHidden(true)
->setDescription(pht('Balanced secret key.')),
);
}
}

View file

@ -1,25 +0,0 @@
<?php
final class PhabricatorStripeConfigOptions
extends PhabricatorApplicationConfigOptions {
public function getName() {
return pht("Integration with Stripe");
}
public function getDescription() {
return pht("Configure Stripe payments.");
}
public function getOptions() {
return array(
$this->newOption('stripe.publishable-key', 'string', null)
->setDescription(
pht('Stripe publishable key.')),
$this->newOption('stripe.secret-key', 'string', null)
->setDescription(
pht('Stripe secret key.')),
);
}
}

View file

@ -0,0 +1,171 @@
<?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 canHandlePaymentMethod(PhortunePaymentMethod $method) {
$type = $method->getMetadataValue('type');
return ($type === 'balanced.account');
}
protected function executeCharge(
PhortunePaymentMethod $method,
PhortuneCharge $charge) {
throw new PhortuneNotImplementedException($this);
}
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;
}
/**
* @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) {
$card_errors = $request->getStr('cardErrors');
$balanced_data = $request->getStr('balancedCardData');
$errors = array();
if ($card_errors) {
$raw_errors = json_decode($card_errors);
$errors = $this->parseRawCreatePaymentMethodErrors($raw_errors);
}
if (!$errors) {
$data = json_decode($balanced_data, true);
if (!is_array($data)) {
$errors[] = pht('An error occurred decoding card data.');
}
}
if (!$errors) {
$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();
$buyer = Balanced\Marketplace::mine()->createBuyer(
null,
$data['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);
}
$exp_string = $data['expiration_year'].'-'.$data['expiration_month'];
$epoch = strtotime($exp_string);
$method
->setName($data['brand'].' / '.$data['last_four'])
->setExpiresEpoch($epoch)
->setMetadata(
array(
'type' => 'balanced.account',
'balanced.accountURI' => $buyer->uri,
'balanced.cardURI' => $data['uri'],
));
}
return $errors;
}
public function renderCreatePaymentMethodForm(
AphrontRequest $request,
array $errors) {
$ccform = id(new PhortuneCreditCardForm())
->setUser($request->getUser())
->setCardNumberError(isset($errors['number']) ? pht('Invalid') : true)
->setCardCVCError(isset($errors['cvc']) ? pht('Invalid') : true)
->setCardExpirationError(isset($errors['exp']) ? pht('Invalid') : null)
->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 parseRawCreatePaymentMethodErrors(array $raw_errors) {
$errors = array();
foreach ($raw_errors as $error) {
switch ($error) {
case 'number':
$errors[$error] = pht('Card number is incorrect or invalid.');
break;
case 'cvc':
$errors[$error] = pht('CVC code is incorrect or invalid.');
break;
case 'exp':
$errors[$error] = pht('Card expiration date is incorrect.');
break;
default:
$errors[] = $error;
break;
}
}
return $errors;
}
}

View file

@ -59,11 +59,11 @@ final class PhortuneStripePaymentProvider extends PhortunePaymentProvider {
}
private function getPublishableKey() {
return PhabricatorEnv::getEnvConfig('stripe.publishable-key');
return PhabricatorEnv::getEnvConfig('phortune.stripe.publishable-key');
}
private function getSecretKey() {
return PhabricatorEnv::getEnvConfig('stripe.secret-key');
return PhabricatorEnv::getEnvConfig('phortune.stripe.secret-key');
}
@ -90,11 +90,13 @@ final class PhortuneStripePaymentProvider extends PhortunePaymentProvider {
if ($card_errors) {
$raw_errors = json_decode($card_errors);
$errors = $this->parseRawCreatePaymentMethodErrors($raw_errors);
} else if (!$stripe_token) {
$errors[] = pht('There was an unknown error processing your card.');
}
$secret_key = $this->getSecretKey();
if (!$errors) {
if (!$stripe_token) {
$errors[] = pht('There was an unknown error processing your card.');
}
}
if (!$errors) {
$root = dirname(phutil_get_library_root('phabricator'));
@ -102,6 +104,8 @@ final class PhortuneStripePaymentProvider extends PhortunePaymentProvider {
try {
// First, make sure the token is valid.
$secret_key = $this->getSecretKey();
$info = id(new Stripe_Token())->retrieve($stripe_token, $secret_key);
$account_phid = $method->getAccountPHID();

View file

@ -0,0 +1,71 @@
/**
* @provides javelin-behavior-balanced-payment-form
* @requires javelin-behavior
* javelin-dom
* javelin-json
* javelin-workflow
* phortune-credit-card-form
*/
JX.behavior('balanced-payment-form', function(config) {
balanced.init(config.balancedMarketplaceURI);
var root = JX.$(config.formID);
var ccform = new JX.PhortuneCreditCardForm(root);
var onsubmit = function(e) {
e.kill();
var cardData = ccform.getCardData();
var errors = [];
if (!balanced.card.isCardNumberValid(cardData.number)) {
errors.push('number');
}
if (!balanced.card.isSecurityCodeValid(cardData.number, cardData.cvc)) {
errors.push('cvc');
}
if (!balanced.card.isExpiryValid(cardData.month, cardData.year)) {
errors.push('expiry');
}
if (errors.length) {
JX.Workflow
.newFromForm(root, {cardErrors: JX.JSON.stringify(errors)})
.start();
return;
}
var data = {
card_number: cardData.number,
security_code: cardData.cvc,
expiration_month: cardData.month,
expiration_year: cardData.year
};
balanced.card.create(data, onresponse);
}
var onresponse = function(response) {
var errors = [];
if (response.error) {
errors = [response.error.type];
} else if (response.status != 201) {
errors = ['balanced:' + response.status];
}
var params = {
cardErrors: JX.JSON.stringify(errors),
balancedCardData: JX.JSON.stringify(response.data)
};
JX.Workflow
.newFromForm(root, params)
.start();
}
JX.DOM.listen(root, 'submit', null, onsubmit);
});

View file

@ -59,8 +59,13 @@ JX.behavior('stripe-payment-form', function(config) {
token = response.id;
}
var params = {
cardErrors: JX.JSON.stringify(errors),
stripeToken: token
};
JX.Workflow
.newFromForm(root, {cardErrors: errors, stripeToken: token})
.newFromForm(root, params)
.start();
}