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:
parent
23786784ef
commit
6efba56448
9 changed files with 308 additions and 35 deletions
|
@ -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(
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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.");
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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.')),
|
||||
);
|
||||
}
|
||||
|
||||
}
|
|
@ -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.')),
|
||||
);
|
||||
}
|
||||
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
|
@ -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();
|
||||
|
|
|
@ -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);
|
||||
});
|
|
@ -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();
|
||||
}
|
||||
|
||||
|
|
Loading…
Reference in a new issue