1
0
Fork 0
mirror of https://we.phorge.it/source/phorge.git synced 2024-11-09 16:32:39 +01:00

General cleanup for adding payment methods in Phortune

Summary:
This has no real behavioral changes (except better error handling), it just factors things out to be a bit cleaner. In particular:

  - Move more shared form behaviors into the common JS form component.
  - Move more error handling into shared pathways.
  - Make the specialized Stripe / Balanced methods do less work.

This needs some more polish for nontrival errors (especially on the Balanced side) but none of the error behavior is worse than it was and a lot of it is much better.

Ref T2787.

Test Plan: Hit all invalid form errors, added valid payment methods with Stripe and Balacned.

Reviewers: btrahan, chad

Reviewed By: btrahan

CC: aran

Maniphest Tasks: T2787

Differential Revision: https://secure.phabricator.com/D5771
This commit is contained in:
epriestley 2013-04-25 09:49:32 -07:00
parent 6efba56448
commit 7a5f622820
16 changed files with 443 additions and 287 deletions

View file

@ -0,0 +1,19 @@
TRUNCATE TABLE {$NAMESPACE}_phortune.phortune_paymentmethod;
ALTER TABLE {$NAMESPACE}_phortune.phortune_paymentmethod
ADD brand VARCHAR(64) NOT NULL;
ALTER TABLE {$NAMESPACE}_phortune.phortune_paymentmethod
ADD expires VARCHAR(16) NOT NULL;
ALTER TABLE {$NAMESPACE}_phortune.phortune_paymentmethod
ADD providerType VARCHAR(16) NOT NULL;
ALTER TABLE {$NAMESPACE}_phortune.phortune_paymentmethod
ADD providerDomain VARCHAR(64) NOT NULL;
ALTER TABLE {$NAMESPACE}_phortune.phortune_paymentmethod
ADD lastFourDigits VARCHAR(16) NOT NULL;
ALTER TABLE {$NAMESPACE}_phortune.phortune_paymentmethod
DROP expiresEpoch;

View file

@ -1276,15 +1276,13 @@ celerity_register_resource_map(array(
),
'javelin-behavior-balanced-payment-form' =>
array(
'uri' => '/res/2a850a31/rsrc/js/application/phortune/behavior-balanced-payment-form.js',
'uri' => '/res/6876492d/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',
2 => 'phortune-credit-card-form',
),
'disk' => '/rsrc/js/application/phortune/behavior-balanced-payment-form.js',
),
@ -2272,15 +2270,13 @@ celerity_register_resource_map(array(
),
'javelin-behavior-stripe-payment-form' =>
array(
'uri' => '/res/2ae12d96/rsrc/js/application/phortune/behavior-stripe-payment-form.js',
'uri' => '/res/c1a12d77/rsrc/js/application/phortune/behavior-stripe-payment-form.js',
'type' => 'js',
'requires' =>
array(
0 => 'javelin-behavior',
1 => 'javelin-dom',
2 => 'javelin-json',
3 => 'javelin-workflow',
4 => 'phortune-credit-card-form',
2 => 'phortune-credit-card-form',
),
'disk' => '/rsrc/js/application/phortune/behavior-stripe-payment-form.js',
),
@ -3605,12 +3601,15 @@ celerity_register_resource_map(array(
),
'phortune-credit-card-form' =>
array(
'uri' => '/res/7be5799a/rsrc/js/application/phortune/phortune-credit-card-form.js',
'uri' => '/res/bc948778/rsrc/js/application/phortune/phortune-credit-card-form.js',
'type' => 'js',
'requires' =>
array(
0 => 'javelin-install',
1 => 'javelin-dom',
2 => 'javelin-json',
3 => 'javelin-workflow',
4 => 'javelin-util',
),
'disk' => '/rsrc/js/application/phortune/phortune-credit-card-form.js',
),

View file

@ -1583,9 +1583,11 @@ phutil_register_library_map(array(
'PhortuneBalancedPaymentProvider' => 'applications/phortune/provider/PhortuneBalancedPaymentProvider.php',
'PhortuneCart' => 'applications/phortune/storage/PhortuneCart.php',
'PhortuneCharge' => 'applications/phortune/storage/PhortuneCharge.php',
'PhortuneConstants' => 'applications/phortune/constants/PhortuneConstants.php',
'PhortuneController' => 'applications/phortune/controller/PhortuneController.php',
'PhortuneCreditCardForm' => 'applications/phortune/view/PhortuneCreditCardForm.php',
'PhortuneDAO' => 'applications/phortune/storage/PhortuneDAO.php',
'PhortuneErrCode' => 'applications/phortune/constants/PhortuneErrCode.php',
'PhortuneLandingController' => 'applications/phortune/controller/PhortuneLandingController.php',
'PhortuneMonthYearExpiryControl' => 'applications/phortune/control/PhortuneMonthYearExpiryControl.php',
'PhortuneMultiplePaymentProvidersException' => 'applications/phortune/exception/PhortuneMultiplePaymentProvidersException.php',
@ -3308,6 +3310,7 @@ phutil_register_library_map(array(
'PhortuneCharge' => 'PhortuneDAO',
'PhortuneController' => 'PhabricatorController',
'PhortuneDAO' => 'PhabricatorLiskDAO',
'PhortuneErrCode' => 'PhortuneConstants',
'PhortuneLandingController' => 'PhortuneController',
'PhortuneMonthYearExpiryControl' => 'AphrontFormControl',
'PhortuneMultiplePaymentProvidersException' => 'Exception',

View file

@ -0,0 +1,5 @@
<?php
abstract class PhortuneConstants {
}

View file

@ -0,0 +1,11 @@
<?php
final class PhortuneErrCode extends PhortuneConstants {
// NOTE: These constants also appear in Javascript.
const ERR_CC_INVALID_NUMBER = 'cc:invalid:number';
const ERR_CC_INVALID_CVC = 'cc:invalid:cvc';
const ERR_CC_INVALID_EXPIRY = 'cc:invalid:expiry';
}

View file

@ -111,7 +111,7 @@ final class PhortuneAccountViewController extends PhortuneController {
foreach ($methods as $method) {
$item = new PhabricatorObjectItemView();
$item->setHeader($method->getName());
$item->setHeader($method->getBrand().' / '.$method->getLastFourDigits());
switch ($method->getStatus()) {
case PhortunePaymentMethod::STATUS_ACTIVE:
@ -126,10 +126,6 @@ final class PhortuneAccountViewController extends PhortuneController {
phabricator_datetime($method->getDateCreated(), $user),
$this->getHandle($method->getAuthorPHID())->renderLink()));
if ($method->getExpiresEpoch() < time() + (60 * 60 * 24 * 30)) {
$item->addAttribute(pht('Expires Soon!'));
}
$list->addItem($item);
}

View file

@ -48,9 +48,38 @@ final class PhortunePaymentMethodEditController
->setAccountPHID($account->getPHID())
->setAuthorPHID($user->getPHID())
->setStatus(PhortunePaymentMethod::STATUS_ACTIVE)
->setMetadataValue('providerKey', $provider->getProviderKey());
->setProviderType($provider->getProviderType())
->setProviderDomain($provider->getProviderDomain());
$errors = $provider->createPaymentMethodFromRequest($request, $method);
if (!$errors) {
$errors = $this->processClientErrors(
$provider,
$request->getStr('errors'));
}
if (!$errors) {
$client_token_raw = $request->getStr('token');
$client_token = json_decode($client_token_raw, true);
if (!is_array($client_token)) {
$errors[] = pht(
'There was an error decoding token information submitted by the '.
'client. Expected a JSON-encoded token dictionary, received: %s.',
nonempty($client_token_raw, pht('nothing')));
} else {
if (!$provider->validateCreatePaymentMethodToken($client_token)) {
$errors[] = pht(
'There was an error with the payment token submitted by the '.
'client. Expected a valid dictionary, received: %s.',
$client_token_raw);
}
}
if (!$errors) {
$errors = $provider->createPaymentMethodFromRequest(
$request,
$method,
$client_token);
}
}
if (!$errors) {
$method->save();
@ -152,4 +181,61 @@ final class PhortunePaymentMethodEditController
));
}
private function processClientErrors(
PhortunePaymentProvider $provider,
$client_errors_raw) {
$errors = array();
$client_errors = json_decode($client_errors_raw, true);
if (!is_array($client_errors)) {
$errors[] = pht(
'There was an error decoding error information submitted by the '.
'client. Expected a JSON-encoded list of error codes, received: %s.',
nonempty($client_errors_raw, pht('nothing')));
}
foreach (array_unique($client_errors) as $key => $client_error) {
$client_errors[$key] = $provider->translateCreatePaymentMethodErrorCode(
$client_error);
}
foreach (array_unique($client_errors) as $client_error) {
switch ($client_error) {
case PhortuneErrCode::ERR_CC_INVALID_NUMBER:
$message = pht(
'The card number you entered is not a valid card number. Check '.
'that you entered it correctly.');
break;
case PhortuneErrCode::ERR_CC_INVALID_CVC:
$message = pht(
'The CVC code you entered is not a valid CVC code. Check that '.
'you entered it correctly. The CVC code is a 3-digit or 4-digit '.
'numeric code which usually appears on the back of the card.');
break;
case PhortuneErrCode::ERR_CC_INVALID_EXPIRY:
$message = pht(
'The card expiration date is not a valid expiration date. Check '.
'that you entered it correctly. You can not add an expired card '.
'as a payment method.');
break;
default:
$message = $provider->getCreatePaymentErrorMessage($client_error);
if (!$message) {
$message = pht(
"There was an unexpected error ('%s') processing payment ".
"information.",
$client_error);
phlog($message);
}
break;
}
$errors[$client_error] = $message;
}
return $errors;
}
}

View file

@ -55,71 +55,61 @@ final class PhortuneBalancedPaymentProvider extends PhortunePaymentProvider {
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) {
$card_errors = $request->getStr('cardErrors');
$balanced_data = $request->getStr('balancedCardData');
PhortunePaymentMethod $method,
array $token) {
$errors = array();
if ($card_errors) {
$raw_errors = json_decode($card_errors);
$errors = $this->parseRawCreatePaymentMethodErrors($raw_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();
$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);
}
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'],
));
}
$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;
}
@ -130,9 +120,7 @@ final class PhortuneBalancedPaymentProvider extends PhortunePaymentProvider {
$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)
->setErrors($errors)
->addScript('https://js.balancedpayments.com/v1/balanced.js');
Javelin::initBehavior(
@ -145,27 +133,43 @@ final class PhortuneBalancedPaymentProvider extends PhortunePaymentProvider {
return $ccform->buildForm();
}
private function parseRawCreatePaymentMethodErrors(array $raw_errors) {
$errors = array();
private function getBalancedShortErrorCode($error_code) {
$prefix = 'cc:balanced:';
if (strncmp($error_code, $prefix, strlen($prefix))) {
return null;
}
return substr($error_code, strlen($prefix));
}
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;
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 $errors;
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;
}
}

View file

@ -92,12 +92,37 @@ abstract class PhortunePaymentProvider {
}
/**
* @task addmethod
*/
public function translateCreatePaymentMethodErrorCode($error_code) {
throw new PhortuneNotImplementedException($this);
}
/**
* @task addmethod
*/
public function getCreatePaymentMethodErrorMessage($error_code) {
throw new PhortuneNotImplementedException($this);
}
/**
* @task addmethod
*/
public function validateCreatePaymentMethodToken(array $token) {
throw new PhortuneNotImplementedException($this);
}
/**
* @task addmethod
*/
public function createPaymentMethodFromRequest(
AphrontRequest $request,
PhortunePaymentMethod $method) {
PhortunePaymentMethod $method,
array $token) {
throw new PhortuneNotImplementedException($this);
}

View file

@ -81,63 +81,45 @@ final class PhortuneStripePaymentProvider extends PhortunePaymentProvider {
*/
public function createPaymentMethodFromRequest(
AphrontRequest $request,
PhortunePaymentMethod $method) {
$card_errors = $request->getStr('cardErrors');
$stripe_token = $request->getStr('stripeToken');
PhortunePaymentMethod $method,
array $token) {
$errors = array();
if ($card_errors) {
$raw_errors = json_decode($card_errors);
$errors = $this->parseRawCreatePaymentMethodErrors($raw_errors);
}
if (!$errors) {
if (!$stripe_token) {
$errors[] = pht('There was an unknown error processing your card.');
}
}
$root = dirname(phutil_get_library_root('phabricator'));
require_once $root.'/externals/stripe-php/lib/Stripe.php';
if (!$errors) {
$root = dirname(phutil_get_library_root('phabricator'));
require_once $root.'/externals/stripe-php/lib/Stripe.php';
$secret_key = $this->getSecretKey();
$stripe_token = $token['stripeCardToken'];
try {
// First, make sure the token is valid.
$secret_key = $this->getSecretKey();
// First, make sure the token is valid.
$info = id(new Stripe_Token())->retrieve($stripe_token, $secret_key);
$info = id(new Stripe_Token())->retrieve($stripe_token, $secret_key);
$account_phid = $method->getAccountPHID();
$author_phid = $method->getAuthorPHID();
$account_phid = $method->getAccountPHID();
$author_phid = $method->getAuthorPHID();
$params = array(
'card' => $stripe_token,
'description' => $account_phid.':'.$author_phid,
);
$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);
// 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
->setName($card->type.' / '.$card->last4)
->setExpiresEpoch(strtotime($card->exp_year.'-'.$card->exp_month))
->setMetadata(
array(
'type' => 'stripe.customer',
'stripe.customerID' => $customer->id,
'stripe.tokenID' => $stripe_token,
));
} catch (Exception $ex) {
phlog($ex);
$errors[] = pht(
'There was an error communicating with the payments backend.');
}
}
$card = $info->card;
$method
->setBrand($card->type)
->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;
}
@ -148,9 +130,7 @@ final class PhortuneStripePaymentProvider extends PhortunePaymentProvider {
$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)
->setErrors($errors)
->addScript('https://js.stripe.com/v2/');
Javelin::initBehavior(
@ -163,64 +143,84 @@ final class PhortuneStripePaymentProvider extends PhortunePaymentProvider {
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));
}
/**
* Stripe JS and calls to Stripe handle all errors with processing this
* form. This function takes the raw errors - in the form of an array
* where each elementt is $type => $message - and figures out what if
* any fields were invalid and pulls the messages into a flat object.
*
* See https://stripe.com/docs/api#errors for more information on possible
* errors.
*/
private function parseRawCreatePaymentMethodErrors(array $raw_errors) {
$errors = array();
public function validateCreatePaymentMethodToken(array $token) {
return isset($token['stripeCardToken']);
}
foreach ($raw_errors as $type) {
$error_key = null;
$message = pht('A card processing error has occurred.');
switch ($type) {
case 'number':
case 'invalid_number':
case 'incorrect_number':
$error_key = 'number';
$message = pht('Invalid or incorrect credit card number.');
break;
case 'cvc':
case 'invalid_cvc':
case 'incorrect_cvc':
$error_key = 'cvc';
$message = pht('Card CVC is invalid or incorrect.');
break;
case 'expiry':
case 'invalid_expiry_month':
case 'invalid_expiry_year':
$error_key = 'exp';
$message = pht('Card expiration date is invalid or incorrect.');
break;
case 'card_declined':
case 'expired_card':
case 'duplicate_transaction':
case 'processing_error':
// these errors don't map well to field(s) being bad
break;
case 'invalid_amount':
case 'missing':
default:
// these errors only happen if we (not the user) messed up so log it
$error = sprintf('[Stripe Error] %s', $type);
phlog($error);
break;
}
public function translateCreatePaymentMethodErrorCode($error_code) {
$short_code = $this->getStripeShortErrorCode($error_code);
if ($error_key === null || isset($errors[$error_key])) {
$errors[] = $message;
} else {
$errors[$error_key] = $message;
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 $errors;
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;
}
}

View file

@ -11,12 +11,16 @@ final class PhortunePaymentMethod extends PhortuneDAO
const STATUS_FAILED = 'payment:failed';
const STATUS_REMOVED = 'payment:removed';
protected $name;
protected $name = '';
protected $status;
protected $accountPHID;
protected $authorPHID;
protected $expiresEpoch;
protected $expires;
protected $metadata = array();
protected $brand;
protected $lastFourDigits;
protected $providerType;
protected $providerDomain;
private $account;
@ -47,7 +51,7 @@ final class PhortunePaymentMethod extends PhortuneDAO
}
public function getDescription() {
return pht('Expires %s', date('m/y'), $this->getExpiresEpoch());
return '...';
}
public function getMetadataValue($key, $default = null) {
@ -80,6 +84,11 @@ final class PhortunePaymentMethod extends PhortuneDAO
return head($accept);
}
public function setExpires($year, $month) {
$this->expires = $year.'-'.$month;
return $this;
}
/* -( PhabricatorPolicyInterface )----------------------------------------- */

View file

@ -5,6 +5,7 @@ final class PhortuneCreditCardForm {
private $formID;
private $scripts = array();
private $user;
private $errors = array();
private $cardNumberError;
private $cardCVCError;
@ -15,18 +16,8 @@ final class PhortuneCreditCardForm {
return $this;
}
public function setCardExpirationError($card_expiration_error) {
$this->cardExpirationError = $card_expiration_error;
return $this;
}
public function setCardCVCError($card_cvc_error) {
$this->cardCVCError = $card_cvc_error;
return $this;
}
public function setCardNumberError($card_number_error) {
$this->cardNumberError = $card_number_error;
public function setErrors(array $errors) {
$this->errors = $errors;
return $this;
}
@ -63,6 +54,19 @@ final class PhortuneCreditCardForm {
)));
}
$errors = $this->errors;
$e_number = isset($errors[PhortuneErrCode::ERR_CC_INVALID_NUMBER])
? pht('Invalid')
: true;
$e_cvc = isset($errors[PhortuneErrCode::ERR_CC_INVALID_CVC])
? pht('Invalid')
: true;
$e_expiry = isset($errors[PhortuneErrCode::ERR_CC_INVALID_EXPIRY])
? pht('Invalid')
: null;
$form
->setID($form_id)
->appendChild(
@ -85,18 +89,18 @@ final class PhortuneCreditCardForm {
->setLabel('Card Number')
->setDisableAutocomplete(true)
->setSigil('number-input')
->setError($this->cardNumberError))
->setError($e_number))
->appendChild(
id(new AphrontFormTextControl())
->setLabel('CVC')
->setDisableAutocomplete(true)
->setSigil('cvc-input')
->setError($this->cardCVCError))
->setError($e_cvc))
->appendChild(
id(new PhortuneMonthYearExpiryControl())
->setLabel('Expiration')
->setUser($this->user)
->setError($this->cardExpirationError));
->setError($e_expiry));
return $form;
}

View file

@ -1246,6 +1246,10 @@ final class PhabricatorBuiltinPatchList extends PhabricatorSQLPatchList {
'type' => 'sql',
'name' => $this->getPatchPath('20130423.updateexternalaccount.sql'),
),
'20130423.phortunepaymentrevised.sql' => array(
'type' => 'sql',
'name' => $this->getPatchPath('20130423.phortunepaymentrevised.sql'),
),
);
}
}

View file

@ -2,70 +2,56 @@
* @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 ccform = new JX.PhortuneCreditCardForm(JX.$(config.formID), onsubmit);
var onsubmit = function(e) {
e.kill();
var cardData = ccform.getCardData();
function onsubmit(card_data) {
var errors = [];
if (!balanced.card.isCardNumberValid(cardData.number)) {
errors.push('number');
if (!balanced.card.isCardNumberValid(card_data.number)) {
errors.push('cc:invalid:number');
}
if (!balanced.card.isSecurityCodeValid(cardData.number, cardData.cvc)) {
errors.push('cvc');
if (!balanced.card.isSecurityCodeValid(card_data.number, card_data.cvc)) {
errors.push('cc:invalid:cvc');
}
if (!balanced.card.isExpiryValid(cardData.month, cardData.year)) {
errors.push('expiry');
if (!balanced.card.isExpiryValid(card_data.month, card_data.year)) {
errors.push('cc:invalid:expiry');
}
if (errors.length) {
JX.Workflow
.newFromForm(root, {cardErrors: JX.JSON.stringify(errors)})
.start();
ccform.submitForm(errors);
return;
}
var data = {
card_number: cardData.number,
security_code: cardData.cvc,
expiration_month: cardData.month,
expiration_year: cardData.year
card_number: card_data.number,
security_code: card_data.cvc,
expiration_month: card_data.month,
expiration_year: card_data.year
};
balanced.card.create(data, onresponse);
}
var onresponse = function(response) {
function onresponse(response) {
var token = null;
var errors = [];
if (response.error) {
errors = [response.error.type];
errors = ['cc:balanced:error:' + response.error.type];
} else if (response.status != 201) {
errors = ['balanced:' + response.status];
errors = ['cc:balanced:http:' + response.status];
} else {
token = response.data.uri;
}
var params = {
cardErrors: JX.JSON.stringify(errors),
balancedCardData: JX.JSON.stringify(response.data)
};
JX.Workflow
.newFromForm(root, params)
.start();
ccform.submitForm(errors, {balancedMarketplaceURI: token});
}
JX.DOM.listen(root, 'submit', null, onsubmit);
});

View file

@ -2,72 +2,56 @@
* @provides javelin-behavior-stripe-payment-form
* @requires javelin-behavior
* javelin-dom
* javelin-json
* javelin-workflow
* phortune-credit-card-form
*/
JX.behavior('stripe-payment-form', function(config) {
Stripe.setPublishableKey(config.stripePublishableKey);
var root = JX.$(config.formID);
var ccform = new JX.PhortuneCreditCardForm(root);
var ccform = new JX.PhortuneCreditCardForm(JX.$(config.formID), onsubmit);
var onsubmit = function(e) {
e.kill();
// validate the card data with Stripe client API and submit the form
// with any detected errors
var cardData = ccform.getCardData();
function onsubmit(card_data) {
var errors = [];
if (!Stripe.validateCardNumber(cardData.number)) {
errors.push('number');
if (!Stripe.validateCardNumber(card_data.number)) {
errors.push('cc:invalid:number');
}
if (!Stripe.validateCVC(cardData.cvc)) {
errors.push('cvc');
if (!Stripe.validateCVC(card_data.cvc)) {
errors.push('cc:invalid:cvc');
}
if (!Stripe.validateExpiry(cardData.month, cardData.year)) {
errors.push('expiry');
if (!Stripe.validateExpiry(card_data.month, card_data.year)) {
errors.push('cc:invalid:expiry');
}
if (errors.length) {
JX.Workflow
.newFromForm(root, {cardErrors: JX.JSON.stringify(errors)})
.start();
ccform.submitForm(errors);
return;
}
var data = {
number: cardData.number,
cvc: cardData.cvc,
exp_month: cardData.month,
exp_year: cardData.year
number: card_data.number,
cvc: card_data.cvc,
exp_month: card_data.month,
exp_year: card_data.year
};
Stripe.createToken(data, onresponse);
}
var onresponse = function(status, response) {
function onresponse(status, response) {
var errors = [];
var token = null;
if (response.error) {
errors = [response.error.type];
if (status != 200) {
errors.push('cc:stripe:http:' + status);
} else if (response.error) {
errors.push('cc:stripe:error:' + response.error.type);
} else {
token = response.id;
}
var params = {
cardErrors: JX.JSON.stringify(errors),
stripeToken: token
};
JX.Workflow
.newFromForm(root, params)
.start();
ccform.submitForm(errors, {stripeCardToken: token});
}
JX.DOM.listen(root, 'submit', null, onsubmit);
});

View file

@ -2,6 +2,10 @@
* @provides phortune-credit-card-form
* @requires javelin-install
* javelin-dom
* javelin-json
* javelin-workflow
* javelin-util
* @javelin
*/
/**
@ -9,21 +13,21 @@
*
* To construct an object for a form:
*
* new JX.PhortuneCreditCardForm(form_root_node);
* new JX.PhortuneCreditCardForm(form_root_node, submit_callback);
*
* To read card data from a form:
*
* var data = ccform.getCardData();
*/
JX.install('PhortuneCreditCardForm', {
construct : function(root) {
construct : function(root, onsubmit) {
this._root = root;
this._submitCallback = onsubmit;
JX.DOM.listen(root, 'submit', null, JX.bind(this, this._onsubmit));
},
members : {
_root : null,
_submitCallback : null,
getCardData : function() {
_getCardData : function() {
var root = this._root;
return {
@ -32,7 +36,24 @@ JX.install('PhortuneCreditCardForm', {
month : JX.DOM.find(root, 'select', 'month-input' ).value,
year : JX.DOM.find(root, 'select', 'year-input' ).value
};
},
submitForm : function(errors, token) {
var params = {
errors: JX.JSON.stringify(errors),
token: JX.JSON.stringify(token || {})
};
JX.Workflow
.newFromForm(this._root, params)
.start();
},
_onsubmit : function(e) {
e.kill();
this._submitCallback(this._getCardData());
}
}
});