1
0
Fork 0
mirror of https://we.phorge.it/source/phorge.git synced 2024-11-14 02:42:40 +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' => 'javelin-behavior-balanced-payment-form' =>
array( 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', 'type' => 'js',
'requires' => 'requires' =>
array( array(
0 => 'javelin-behavior', 0 => 'javelin-behavior',
1 => 'javelin-dom', 1 => 'javelin-dom',
2 => 'javelin-json', 2 => 'phortune-credit-card-form',
3 => 'javelin-workflow',
4 => 'phortune-credit-card-form',
), ),
'disk' => '/rsrc/js/application/phortune/behavior-balanced-payment-form.js', 'disk' => '/rsrc/js/application/phortune/behavior-balanced-payment-form.js',
), ),
@ -2272,15 +2270,13 @@ celerity_register_resource_map(array(
), ),
'javelin-behavior-stripe-payment-form' => 'javelin-behavior-stripe-payment-form' =>
array( 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', 'type' => 'js',
'requires' => 'requires' =>
array( array(
0 => 'javelin-behavior', 0 => 'javelin-behavior',
1 => 'javelin-dom', 1 => 'javelin-dom',
2 => 'javelin-json', 2 => 'phortune-credit-card-form',
3 => 'javelin-workflow',
4 => 'phortune-credit-card-form',
), ),
'disk' => '/rsrc/js/application/phortune/behavior-stripe-payment-form.js', 'disk' => '/rsrc/js/application/phortune/behavior-stripe-payment-form.js',
), ),
@ -3605,12 +3601,15 @@ celerity_register_resource_map(array(
), ),
'phortune-credit-card-form' => 'phortune-credit-card-form' =>
array( 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', 'type' => 'js',
'requires' => 'requires' =>
array( array(
0 => 'javelin-install', 0 => 'javelin-install',
1 => 'javelin-dom', 1 => 'javelin-dom',
2 => 'javelin-json',
3 => 'javelin-workflow',
4 => 'javelin-util',
), ),
'disk' => '/rsrc/js/application/phortune/phortune-credit-card-form.js', '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', 'PhortuneBalancedPaymentProvider' => 'applications/phortune/provider/PhortuneBalancedPaymentProvider.php',
'PhortuneCart' => 'applications/phortune/storage/PhortuneCart.php', 'PhortuneCart' => 'applications/phortune/storage/PhortuneCart.php',
'PhortuneCharge' => 'applications/phortune/storage/PhortuneCharge.php', 'PhortuneCharge' => 'applications/phortune/storage/PhortuneCharge.php',
'PhortuneConstants' => 'applications/phortune/constants/PhortuneConstants.php',
'PhortuneController' => 'applications/phortune/controller/PhortuneController.php', 'PhortuneController' => 'applications/phortune/controller/PhortuneController.php',
'PhortuneCreditCardForm' => 'applications/phortune/view/PhortuneCreditCardForm.php', 'PhortuneCreditCardForm' => 'applications/phortune/view/PhortuneCreditCardForm.php',
'PhortuneDAO' => 'applications/phortune/storage/PhortuneDAO.php', 'PhortuneDAO' => 'applications/phortune/storage/PhortuneDAO.php',
'PhortuneErrCode' => 'applications/phortune/constants/PhortuneErrCode.php',
'PhortuneLandingController' => 'applications/phortune/controller/PhortuneLandingController.php', 'PhortuneLandingController' => 'applications/phortune/controller/PhortuneLandingController.php',
'PhortuneMonthYearExpiryControl' => 'applications/phortune/control/PhortuneMonthYearExpiryControl.php', 'PhortuneMonthYearExpiryControl' => 'applications/phortune/control/PhortuneMonthYearExpiryControl.php',
'PhortuneMultiplePaymentProvidersException' => 'applications/phortune/exception/PhortuneMultiplePaymentProvidersException.php', 'PhortuneMultiplePaymentProvidersException' => 'applications/phortune/exception/PhortuneMultiplePaymentProvidersException.php',
@ -3308,6 +3310,7 @@ phutil_register_library_map(array(
'PhortuneCharge' => 'PhortuneDAO', 'PhortuneCharge' => 'PhortuneDAO',
'PhortuneController' => 'PhabricatorController', 'PhortuneController' => 'PhabricatorController',
'PhortuneDAO' => 'PhabricatorLiskDAO', 'PhortuneDAO' => 'PhabricatorLiskDAO',
'PhortuneErrCode' => 'PhortuneConstants',
'PhortuneLandingController' => 'PhortuneController', 'PhortuneLandingController' => 'PhortuneController',
'PhortuneMonthYearExpiryControl' => 'AphrontFormControl', 'PhortuneMonthYearExpiryControl' => 'AphrontFormControl',
'PhortuneMultiplePaymentProvidersException' => 'Exception', '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) { foreach ($methods as $method) {
$item = new PhabricatorObjectItemView(); $item = new PhabricatorObjectItemView();
$item->setHeader($method->getName()); $item->setHeader($method->getBrand().' / '.$method->getLastFourDigits());
switch ($method->getStatus()) { switch ($method->getStatus()) {
case PhortunePaymentMethod::STATUS_ACTIVE: case PhortunePaymentMethod::STATUS_ACTIVE:
@ -126,10 +126,6 @@ final class PhortuneAccountViewController extends PhortuneController {
phabricator_datetime($method->getDateCreated(), $user), phabricator_datetime($method->getDateCreated(), $user),
$this->getHandle($method->getAuthorPHID())->renderLink())); $this->getHandle($method->getAuthorPHID())->renderLink()));
if ($method->getExpiresEpoch() < time() + (60 * 60 * 24 * 30)) {
$item->addAttribute(pht('Expires Soon!'));
}
$list->addItem($item); $list->addItem($item);
} }

View file

@ -48,9 +48,38 @@ final class PhortunePaymentMethodEditController
->setAccountPHID($account->getPHID()) ->setAccountPHID($account->getPHID())
->setAuthorPHID($user->getPHID()) ->setAuthorPHID($user->getPHID())
->setStatus(PhortunePaymentMethod::STATUS_ACTIVE) ->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) { if (!$errors) {
$method->save(); $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,33 +55,24 @@ final class PhortuneBalancedPaymentProvider extends PhortunePaymentProvider {
return true; 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\Settings
* @phutil-external-symbol class Balanced\Marketplace * @phutil-external-symbol class Balanced\Marketplace
* @phutil-external-symbol class RESTful\Exceptions\HTTPError * @phutil-external-symbol class RESTful\Exceptions\HTTPError
*/ */
public function createPaymentMethodFromRequest( public function createPaymentMethodFromRequest(
AphrontRequest $request, AphrontRequest $request,
PhortunePaymentMethod $method) { PhortunePaymentMethod $method,
array $token) {
$card_errors = $request->getStr('cardErrors');
$balanced_data = $request->getStr('balancedCardData');
$errors = array(); $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')); $root = dirname(phutil_get_library_root('phabricator'));
require_once $root.'/externals/httpful/bootstrap.php'; require_once $root.'/externals/httpful/bootstrap.php';
require_once $root.'/externals/restful/bootstrap.php'; require_once $root.'/externals/restful/bootstrap.php';
@ -92,11 +83,13 @@ final class PhortuneBalancedPaymentProvider extends PhortunePaymentProvider {
$description = $account_phid.':'.$author_phid; $description = $account_phid.':'.$author_phid;
try { try {
Balanced\Settings::$api_key = $this->getSecretKey(); Balanced\Settings::$api_key = $this->getSecretKey();
$card = Balanced\Card::get($token['balancedMarketplaceURI']);
$buyer = Balanced\Marketplace::mine()->createBuyer( $buyer = Balanced\Marketplace::mine()->createBuyer(
null, null,
$data['uri'], $card->uri,
array( array(
'description' => $description, 'description' => $description,
)); ));
@ -107,19 +100,16 @@ final class PhortuneBalancedPaymentProvider extends PhortunePaymentProvider {
throw new Exception($error->response->body->description); throw new Exception($error->response->body->description);
} }
$exp_string = $data['expiration_year'].'-'.$data['expiration_month'];
$epoch = strtotime($exp_string);
$method $method
->setName($data['brand'].' / '.$data['last_four']) ->setBrand($card->brand)
->setExpiresEpoch($epoch) ->setLastFourDigits($card->last_four)
->setExpires($card->expiration_year, $card->expiration_month)
->setMetadata( ->setMetadata(
array( array(
'type' => 'balanced.account', 'type' => 'balanced.account',
'balanced.accountURI' => $buyer->uri, 'balanced.accountURI' => $buyer->uri,
'balanced.cardURI' => $data['uri'], 'balanced.cardURI' => $card->uri,
)); ));
}
return $errors; return $errors;
} }
@ -130,9 +120,7 @@ final class PhortuneBalancedPaymentProvider extends PhortunePaymentProvider {
$ccform = id(new PhortuneCreditCardForm()) $ccform = id(new PhortuneCreditCardForm())
->setUser($request->getUser()) ->setUser($request->getUser())
->setCardNumberError(isset($errors['number']) ? pht('Invalid') : true) ->setErrors($errors)
->setCardCVCError(isset($errors['cvc']) ? pht('Invalid') : true)
->setCardExpirationError(isset($errors['exp']) ? pht('Invalid') : null)
->addScript('https://js.balancedpayments.com/v1/balanced.js'); ->addScript('https://js.balancedpayments.com/v1/balanced.js');
Javelin::initBehavior( Javelin::initBehavior(
@ -145,27 +133,43 @@ final class PhortuneBalancedPaymentProvider extends PhortunePaymentProvider {
return $ccform->buildForm(); return $ccform->buildForm();
} }
private function parseRawCreatePaymentMethodErrors(array $raw_errors) { private function getBalancedShortErrorCode($error_code) {
$errors = array(); $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) {
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: default:
$errors[] = $error;
break; break;
} }
}
return $errors;
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 * @task addmethod
*/ */
public function createPaymentMethodFromRequest( public function createPaymentMethodFromRequest(
AphrontRequest $request, AphrontRequest $request,
PhortunePaymentMethod $method) { PhortunePaymentMethod $method,
array $token) {
throw new PhortuneNotImplementedException($this); throw new PhortuneNotImplementedException($this);
} }

View file

@ -81,31 +81,18 @@ final class PhortuneStripePaymentProvider extends PhortunePaymentProvider {
*/ */
public function createPaymentMethodFromRequest( public function createPaymentMethodFromRequest(
AphrontRequest $request, AphrontRequest $request,
PhortunePaymentMethod $method) { PhortunePaymentMethod $method,
array $token) {
$card_errors = $request->getStr('cardErrors');
$stripe_token = $request->getStr('stripeToken');
$errors = array(); $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.');
}
}
if (!$errors) {
$root = dirname(phutil_get_library_root('phabricator')); $root = dirname(phutil_get_library_root('phabricator'));
require_once $root.'/externals/stripe-php/lib/Stripe.php'; require_once $root.'/externals/stripe-php/lib/Stripe.php';
try {
// First, make sure the token is valid.
$secret_key = $this->getSecretKey(); $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); $info = id(new Stripe_Token())->retrieve($stripe_token, $secret_key);
$account_phid = $method->getAccountPHID(); $account_phid = $method->getAccountPHID();
@ -124,20 +111,15 @@ final class PhortuneStripePaymentProvider extends PhortunePaymentProvider {
$card = $info->card; $card = $info->card;
$method $method
->setName($card->type.' / '.$card->last4) ->setBrand($card->type)
->setExpiresEpoch(strtotime($card->exp_year.'-'.$card->exp_month)) ->setLastFourDigits($card->last4)
->setExpires($card->exp_year, $card->exp_month)
->setMetadata( ->setMetadata(
array( array(
'type' => 'stripe.customer', 'type' => 'stripe.customer',
'stripe.customerID' => $customer->id, 'stripe.customerID' => $customer->id,
'stripe.tokenID' => $stripe_token, 'stripe.cardToken' => $stripe_token,
)); ));
} catch (Exception $ex) {
phlog($ex);
$errors[] = pht(
'There was an error communicating with the payments backend.');
}
}
return $errors; return $errors;
} }
@ -148,9 +130,7 @@ final class PhortuneStripePaymentProvider extends PhortunePaymentProvider {
$ccform = id(new PhortuneCreditCardForm()) $ccform = id(new PhortuneCreditCardForm())
->setUser($request->getUser()) ->setUser($request->getUser())
->setCardNumberError(isset($errors['number']) ? pht('Invalid') : true) ->setErrors($errors)
->setCardCVCError(isset($errors['cvc']) ? pht('Invalid') : true)
->setCardExpirationError(isset($errors['exp']) ? pht('Invalid') : null)
->addScript('https://js.stripe.com/v2/'); ->addScript('https://js.stripe.com/v2/');
Javelin::initBehavior( Javelin::initBehavior(
@ -163,64 +143,84 @@ final class PhortuneStripePaymentProvider extends PhortunePaymentProvider {
return $ccform->buildForm(); 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;
}
/** /**
* 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 * See https://stripe.com/docs/api#errors for more information on possible
* errors. * errors.
*/ */
private function parseRawCreatePaymentMethodErrors(array $raw_errors) { public function getCreatePaymentMethodErrorMessage($error_code) {
$errors = array(); $short_code = $this->getStripeShortErrorCode($error_code);
if (!$short_code) {
return null;
}
foreach ($raw_errors as $type) { switch ($short_code) {
$error_key = null; case 'error:incorrect_number':
$message = pht('A card processing error has occurred.');
switch ($type) {
case 'number':
case 'invalid_number':
case 'incorrect_number':
$error_key = 'number'; $error_key = 'number';
$message = pht('Invalid or incorrect credit card number.'); $message = pht('Invalid or incorrect credit card number.');
break; break;
case 'cvc': case 'error:incorrect_cvc':
case 'invalid_cvc':
case 'incorrect_cvc':
$error_key = 'cvc'; $error_key = 'cvc';
$message = pht('Card CVC is invalid or incorrect.'); $message = pht('Card CVC is invalid or incorrect.');
break; break;
case 'expiry':
case 'invalid_expiry_month':
case 'invalid_expiry_year':
$error_key = 'exp'; $error_key = 'exp';
$message = pht('Card expiration date is invalid or incorrect.'); $message = pht('Card expiration date is invalid or incorrect.');
break; break;
case 'card_declined': case 'error:invalid_expiry_month':
case 'expired_card': case 'error:invalid_expiry_year':
case 'duplicate_transaction': case 'error:invalid_cvc':
case 'processing_error': case 'error:invalid_number':
// these errors don't map well to field(s) being bad // 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; break;
case 'invalid_amount': case 'error:invalid_amount':
case 'missing': case 'error:missing':
case 'error:card_declined':
case 'error:expired_card':
case 'error:duplicate_transaction':
case 'error:processing_error':
default: default:
// these errors only happen if we (not the user) messed up so log it // NOTE: These errors currently don't recevive a detailed message.
$error = sprintf('[Stripe Error] %s', $type); // NOTE: We can also end up here with "http:nnn" messages.
phlog($error);
// TODO: At least some of these should have a better message, or be
// translated into common errors above.
break; break;
} }
if ($error_key === null || isset($errors[$error_key])) { return null;
$errors[] = $message;
} else {
$errors[$error_key] = $message;
}
}
return $errors;
} }
} }

View file

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

View file

@ -5,6 +5,7 @@ final class PhortuneCreditCardForm {
private $formID; private $formID;
private $scripts = array(); private $scripts = array();
private $user; private $user;
private $errors = array();
private $cardNumberError; private $cardNumberError;
private $cardCVCError; private $cardCVCError;
@ -15,18 +16,8 @@ final class PhortuneCreditCardForm {
return $this; return $this;
} }
public function setCardExpirationError($card_expiration_error) { public function setErrors(array $errors) {
$this->cardExpirationError = $card_expiration_error; $this->errors = $errors;
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;
return $this; 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 $form
->setID($form_id) ->setID($form_id)
->appendChild( ->appendChild(
@ -85,18 +89,18 @@ final class PhortuneCreditCardForm {
->setLabel('Card Number') ->setLabel('Card Number')
->setDisableAutocomplete(true) ->setDisableAutocomplete(true)
->setSigil('number-input') ->setSigil('number-input')
->setError($this->cardNumberError)) ->setError($e_number))
->appendChild( ->appendChild(
id(new AphrontFormTextControl()) id(new AphrontFormTextControl())
->setLabel('CVC') ->setLabel('CVC')
->setDisableAutocomplete(true) ->setDisableAutocomplete(true)
->setSigil('cvc-input') ->setSigil('cvc-input')
->setError($this->cardCVCError)) ->setError($e_cvc))
->appendChild( ->appendChild(
id(new PhortuneMonthYearExpiryControl()) id(new PhortuneMonthYearExpiryControl())
->setLabel('Expiration') ->setLabel('Expiration')
->setUser($this->user) ->setUser($this->user)
->setError($this->cardExpirationError)); ->setError($e_expiry));
return $form; return $form;
} }

View file

@ -1246,6 +1246,10 @@ final class PhabricatorBuiltinPatchList extends PhabricatorSQLPatchList {
'type' => 'sql', 'type' => 'sql',
'name' => $this->getPatchPath('20130423.updateexternalaccount.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 * @provides javelin-behavior-balanced-payment-form
* @requires javelin-behavior * @requires javelin-behavior
* javelin-dom * javelin-dom
* javelin-json
* javelin-workflow
* phortune-credit-card-form * phortune-credit-card-form
*/ */
JX.behavior('balanced-payment-form', function(config) { JX.behavior('balanced-payment-form', function(config) {
balanced.init(config.balancedMarketplaceURI); balanced.init(config.balancedMarketplaceURI);
var root = JX.$(config.formID); var ccform = new JX.PhortuneCreditCardForm(JX.$(config.formID), onsubmit);
var ccform = new JX.PhortuneCreditCardForm(root);
var onsubmit = function(e) { function onsubmit(card_data) {
e.kill();
var cardData = ccform.getCardData();
var errors = []; var errors = [];
if (!balanced.card.isCardNumberValid(cardData.number)) { if (!balanced.card.isCardNumberValid(card_data.number)) {
errors.push('number'); errors.push('cc:invalid:number');
} }
if (!balanced.card.isSecurityCodeValid(cardData.number, cardData.cvc)) { if (!balanced.card.isSecurityCodeValid(card_data.number, card_data.cvc)) {
errors.push('cvc'); errors.push('cc:invalid:cvc');
} }
if (!balanced.card.isExpiryValid(cardData.month, cardData.year)) { if (!balanced.card.isExpiryValid(card_data.month, card_data.year)) {
errors.push('expiry'); errors.push('cc:invalid:expiry');
} }
if (errors.length) { if (errors.length) {
JX.Workflow ccform.submitForm(errors);
.newFromForm(root, {cardErrors: JX.JSON.stringify(errors)})
.start();
return; return;
} }
var data = { var data = {
card_number: cardData.number, card_number: card_data.number,
security_code: cardData.cvc, security_code: card_data.cvc,
expiration_month: cardData.month, expiration_month: card_data.month,
expiration_year: cardData.year expiration_year: card_data.year
}; };
balanced.card.create(data, onresponse); balanced.card.create(data, onresponse);
} }
var onresponse = function(response) { function onresponse(response) {
var token = null;
var errors = []; var errors = [];
if (response.error) { if (response.error) {
errors = [response.error.type]; errors = ['cc:balanced:error:' + response.error.type];
} else if (response.status != 201) { } else if (response.status != 201) {
errors = ['balanced:' + response.status]; errors = ['cc:balanced:http:' + response.status];
} else {
token = response.data.uri;
} }
var params = { ccform.submitForm(errors, {balancedMarketplaceURI: token});
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

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

View file

@ -2,6 +2,10 @@
* @provides phortune-credit-card-form * @provides phortune-credit-card-form
* @requires javelin-install * @requires javelin-install
* javelin-dom * javelin-dom
* javelin-json
* javelin-workflow
* javelin-util
* @javelin
*/ */
/** /**
@ -9,21 +13,21 @@
* *
* To construct an object for a form: * 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', { JX.install('PhortuneCreditCardForm', {
construct : function(root) { construct : function(root, onsubmit) {
this._root = root; this._root = root;
this._submitCallback = onsubmit;
JX.DOM.listen(root, 'submit', null, JX.bind(this, this._onsubmit));
}, },
members : { members : {
_root : null, _root : null,
_submitCallback : null,
getCardData : function() { _getCardData : function() {
var root = this._root; var root = this._root;
return { return {
@ -32,7 +36,24 @@ JX.install('PhortuneCreditCardForm', {
month : JX.DOM.find(root, 'select', 'month-input' ).value, month : JX.DOM.find(root, 'select', 'month-input' ).value,
year : JX.DOM.find(root, 'select', 'year-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());
} }
} }
}); });