mirror of
https://we.phorge.it/source/phorge.git
synced 2024-11-25 16:22:43 +01:00
Phortune v0.1: add payment methods
Summary: Hook @btrahan's Stripe form to the rest of Phortune. - Users can add payment methods. - They are saved to Stripe and associated with PhortunePaymentMethods on our side. - Payment methods appear on account overview. Test Plan: {F37548} {F37549} {F37550} Reviewers: chad, btrahan Reviewed By: btrahan CC: aran Maniphest Tasks: T2787 Differential Revision: https://secure.phabricator.com/D5438
This commit is contained in:
parent
960ac3b2a6
commit
4f3b5f0ea9
13 changed files with 521 additions and 184 deletions
14
resources/sql/patches/20130323.phortunepayment.sql
Normal file
14
resources/sql/patches/20130323.phortunepayment.sql
Normal file
|
@ -0,0 +1,14 @@
|
|||
CREATE TABLE {$NAMESPACE}_phortune.phortune_paymentmethod (
|
||||
id INT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
|
||||
phid VARCHAR(64) NOT NULL COLLATE utf8_bin,
|
||||
name VARCHAR(255) NOT NULL,
|
||||
status VARCHAR(64) NOT NULL COLLATE utf8_bin,
|
||||
accountPHID VARCHAR(64) NOT NULL COLLATE utf8_bin,
|
||||
authorPHID VARCHAR(64) NOT NULL COLLATE utf8_bin,
|
||||
expiresEpoch INT UNSIGNED,
|
||||
metadata LONGTEXT NOT NULL COLLATE utf8_bin,
|
||||
dateCreated INT UNSIGNED NOT NULL,
|
||||
dateModified INT UNSIGNED NOT NULL,
|
||||
UNIQUE KEY `key_phid` (phid),
|
||||
KEY `key_account` (accountPHID, status)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
|
|
@ -1359,6 +1359,7 @@ 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',
|
||||
|
@ -1540,13 +1541,13 @@ phutil_register_library_map(array(
|
|||
'PhortuneLandingController' => 'applications/phortune/controller/PhortuneLandingController.php',
|
||||
'PhortuneMonthYearExpiryControl' => 'applications/phortune/control/PhortuneMonthYearExpiryControl.php',
|
||||
'PhortunePaymentMethod' => 'applications/phortune/storage/PhortunePaymentMethod.php',
|
||||
'PhortunePaymentMethodEditController' => 'applications/phortune/controller/PhortunePaymentMethodEditController.php',
|
||||
'PhortunePaymentMethodListController' => 'applications/phortune/controller/PhortunePaymentMethodListController.php',
|
||||
'PhortunePaymentMethodQuery' => 'applications/phortune/query/PhortunePaymentMethodQuery.php',
|
||||
'PhortunePaymentMethodViewController' => 'applications/phortune/controller/PhortunePaymentMethodViewController.php',
|
||||
'PhortuneProduct' => 'applications/phortune/storage/PhortuneProduct.php',
|
||||
'PhortunePurchase' => 'applications/phortune/storage/PhortunePurchase.php',
|
||||
'PhortuneStripeBaseController' => 'applications/phortune/stripe/controller/PhortuneStripeBaseController.php',
|
||||
'PhortuneStripePaymentFormView' => 'applications/phortune/stripe/view/PhortuneStripePaymentFormView.php',
|
||||
'PhortuneStripeTestPaymentFormController' => 'applications/phortune/stripe/controller/PhortuneStripeTestPaymentFormController.php',
|
||||
'PhortuneStripePaymentFormView' => 'applications/phortune/view/PhortuneStripePaymentFormView.php',
|
||||
'PhrictionActionConstants' => 'applications/phriction/constants/PhrictionActionConstants.php',
|
||||
'PhrictionChangeType' => 'applications/phriction/constants/PhrictionChangeType.php',
|
||||
'PhrictionConstants' => 'applications/phriction/constants/PhrictionConstants.php',
|
||||
|
@ -2977,6 +2978,7 @@ phutil_register_library_map(array(
|
|||
'PhabricatorStorageManagementStatusWorkflow' => 'PhabricatorStorageManagementWorkflow',
|
||||
'PhabricatorStorageManagementUpgradeWorkflow' => 'PhabricatorStorageManagementWorkflow',
|
||||
'PhabricatorStorageManagementWorkflow' => 'PhutilArgumentWorkflow',
|
||||
'PhabricatorStripeConfigOptions' => 'PhabricatorApplicationConfigOptions',
|
||||
'PhabricatorSubscribersQuery' => 'PhabricatorQuery',
|
||||
'PhabricatorSubscriptionsEditController' => 'PhabricatorController',
|
||||
'PhabricatorSubscriptionsEditor' => 'PhabricatorEditor',
|
||||
|
@ -3194,13 +3196,13 @@ phutil_register_library_map(array(
|
|||
0 => 'PhortuneDAO',
|
||||
1 => 'PhabricatorPolicyInterface',
|
||||
),
|
||||
'PhortunePaymentMethodEditController' => 'PhortuneController',
|
||||
'PhortunePaymentMethodListController' => 'PhabricatorController',
|
||||
'PhortunePaymentMethodQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
|
||||
'PhortunePaymentMethodViewController' => 'PhabricatorController',
|
||||
'PhortuneProduct' => 'PhortuneDAO',
|
||||
'PhortunePurchase' => 'PhortuneDAO',
|
||||
'PhortuneStripeBaseController' => 'PhabricatorController',
|
||||
'PhortuneStripePaymentFormView' => 'AphrontView',
|
||||
'PhortuneStripeTestPaymentFormController' => 'PhortuneStripeBaseController',
|
||||
'PhrictionActionConstants' => 'PhrictionConstants',
|
||||
'PhrictionChangeType' => 'PhrictionConstants',
|
||||
'PhrictionContent' =>
|
||||
|
|
|
@ -32,17 +32,14 @@ final class PhabricatorApplicationPhortune extends PhabricatorApplication {
|
|||
'' => 'PhortuneLandingController',
|
||||
'(?P<accountID>\d+)/' => array(
|
||||
'' => 'PhortuneAccountViewController',
|
||||
'paymentmethod/' => array(
|
||||
'edit/' => 'PhortunePaymentMethodEditController',
|
||||
),
|
||||
),
|
||||
|
||||
'account/' => array(
|
||||
'' => 'PhortuneAccountListController',
|
||||
'edit/(?:(?P<id>\d+)/)?' => 'PhortuneAccountEditController',
|
||||
),
|
||||
'paymentmethod/' => array(
|
||||
'' => 'PhortunePaymentMethodListController',
|
||||
'view/(?P<id>\d+)/' => 'PhortunePaymentMethodViewController',
|
||||
'edit/(?:(?P<id>\d+)/)?' => 'PhortunePaymentMethodEditController',
|
||||
),
|
||||
'stripe/' => array(
|
||||
'testpaymentform/' => 'PhortuneStripeTestPaymentFormController',
|
||||
),
|
||||
|
|
|
@ -95,6 +95,40 @@ final class PhortuneAccountViewController extends PhortuneController {
|
|||
->setNoDataString(
|
||||
pht('No payment methods associated with this account.'));
|
||||
|
||||
$methods = id(new PhortunePaymentMethodQuery())
|
||||
->setViewer($user)
|
||||
->withAccountPHIDs(array($account->getPHID()))
|
||||
->withStatus(PhortunePaymentMethodQuery::STATUS_OPEN)
|
||||
->execute();
|
||||
|
||||
if ($methods) {
|
||||
$this->loadHandles(mpull($methods, 'getAuthorPHID'));
|
||||
}
|
||||
|
||||
foreach ($methods as $method) {
|
||||
$item = new PhabricatorObjectItemView();
|
||||
$item->setHeader($method->getName());
|
||||
|
||||
switch ($method->getStatus()) {
|
||||
case PhortunePaymentMethod::STATUS_ACTIVE:
|
||||
$item->addAttribute(pht('Active'));
|
||||
$item->setBarColor('green');
|
||||
break;
|
||||
}
|
||||
|
||||
$item->addAttribute(
|
||||
pht(
|
||||
'Added %s by %s',
|
||||
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);
|
||||
}
|
||||
|
||||
return array(
|
||||
$header,
|
||||
$actions,
|
||||
|
|
|
@ -0,0 +1,298 @@
|
|||
<?php
|
||||
|
||||
final class PhortunePaymentMethodEditController
|
||||
extends PhortuneController {
|
||||
|
||||
private $accountID;
|
||||
|
||||
public function willProcessRequest(array $data) {
|
||||
$this->accountID = $data['accountID'];
|
||||
}
|
||||
|
||||
/**
|
||||
* @phutil-external-symbol class Stripe_Token
|
||||
* @phutil-external-symbol class Stripe_Customer
|
||||
*/
|
||||
public function processRequest() {
|
||||
$request = $this->getRequest();
|
||||
$user = $request->getUser();
|
||||
|
||||
$stripe_publishable_key = PhabricatorEnv::getEnvConfig(
|
||||
'stripe.publishable-key');
|
||||
if (!$stripe_publishable_key) {
|
||||
throw new Exception(
|
||||
"Stripe publishable API key (`stripe.publishable-key`) is ".
|
||||
"not configured.");
|
||||
}
|
||||
|
||||
$stripe_secret_key = PhabricatorEnv::getEnvConfig('stripe.secret-key');
|
||||
if (!$stripe_secret_key) {
|
||||
throw new Exception(
|
||||
"Stripe secret API kye (`stripe.secret-key`) is not configured.");
|
||||
}
|
||||
|
||||
$account = id(new PhortuneAccountQuery())
|
||||
->setViewer($user)
|
||||
->withIDs(array($this->accountID))
|
||||
->executeOne();
|
||||
if (!$account) {
|
||||
return new Aphront404Response();
|
||||
}
|
||||
|
||||
$account_uri = $this->getApplicationURI($account->getID().'/');
|
||||
|
||||
$e_card_number = true;
|
||||
$e_card_cvc = true;
|
||||
$e_card_exp = true;
|
||||
|
||||
$errors = array();
|
||||
if ($request->isFormPost()) {
|
||||
$card_errors = $request->getStr('cardErrors');
|
||||
$stripe_token = $request->getStr('stripeToken');
|
||||
if ($card_errors) {
|
||||
$raw_errors = json_decode($card_errors);
|
||||
list($e_card_number,
|
||||
$e_card_cvc,
|
||||
$e_card_exp,
|
||||
$messages) = $this->parseRawErrors($raw_errors);
|
||||
$errors = array_merge($errors, $messages);
|
||||
} else if (!$stripe_token) {
|
||||
$errors[] = pht('There was an unknown error processing your card.');
|
||||
}
|
||||
|
||||
if (!$errors) {
|
||||
$root = dirname(phutil_get_library_root('phabricator'));
|
||||
require_once $root.'/externals/stripe-php/lib/Stripe.php';
|
||||
|
||||
try {
|
||||
// First, make sure the token is valid.
|
||||
$info = id(new Stripe_Token())
|
||||
->retrieve($stripe_token, $stripe_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(
|
||||
array(
|
||||
'card' => $stripe_token,
|
||||
'description' => $account->getPHID().':'.$user->getUserName(),
|
||||
), $stripe_secret_key);
|
||||
|
||||
$card = $info->card;
|
||||
} catch (Exception $ex) {
|
||||
phlog($ex);
|
||||
$errors[] = pht(
|
||||
'There was an error communicating with the payments backend.');
|
||||
}
|
||||
|
||||
if (!$errors) {
|
||||
$payment_method = id(new PhortunePaymentMethod())
|
||||
->setAccountPHID($account->getPHID())
|
||||
->setAuthorPHID($user->getPHID())
|
||||
->setName($card->type.' / '.$card->last4)
|
||||
->setStatus(PhortunePaymentMethod::STATUS_ACTIVE)
|
||||
->setExpiresEpoch(strtotime($card->exp_year.'-'.$card->exp_month))
|
||||
->setMetadata(
|
||||
array(
|
||||
'type' => 'stripe.customer',
|
||||
'stripeCustomerID' => $customer->id,
|
||||
'stripeTokenID' => $stripe_token,
|
||||
))
|
||||
->save();
|
||||
|
||||
$save_uri = new PhutilURI($account_uri);
|
||||
$save_uri->setFragment('payment');
|
||||
|
||||
return id(new AphrontRedirectResponse())->setURI($save_uri);
|
||||
}
|
||||
}
|
||||
|
||||
$dialog = id(new AphrontDialogView())
|
||||
->setUser($user)
|
||||
->setTitle(pht('Error Adding Card'))
|
||||
->appendChild(id(new AphrontErrorView())->setErrors($errors))
|
||||
->addCancelButton($request->getRequestURI());
|
||||
|
||||
return id(new AphrontDialogResponse())->setDialog($dialog);
|
||||
}
|
||||
|
||||
if ($errors) {
|
||||
$errors = id(new AphrontErrorView())
|
||||
->setErrors($errors);
|
||||
}
|
||||
|
||||
$header = id(new PhabricatorHeaderView())
|
||||
->setHeader(pht('Add New Payment Method'));
|
||||
|
||||
$form_id = celerity_generate_unique_node_id();
|
||||
require_celerity_resource('stripe-payment-form-css');
|
||||
require_celerity_resource('aphront-tooltip-css');
|
||||
Javelin::initBehavior('phabricator-tooltips');
|
||||
|
||||
$form = id(new AphrontFormView())
|
||||
->setID($form_id)
|
||||
->setUser($user)
|
||||
->setWorkflow(true)
|
||||
->setAction($request->getRequestURI())
|
||||
->appendChild(
|
||||
id(new AphrontFormMarkupControl())
|
||||
->setLabel('')
|
||||
->setValue(
|
||||
javelin_tag(
|
||||
'div',
|
||||
array(
|
||||
'class' => 'credit-card-logos',
|
||||
'sigil' => 'has-tooltip',
|
||||
'meta' => array(
|
||||
'tip' => 'We support Visa, Mastercard, American Express, '.
|
||||
'Discover, JCB, and Diners Club.',
|
||||
'size' => 440,
|
||||
)
|
||||
))))
|
||||
->appendChild(
|
||||
id(new AphrontFormTextControl())
|
||||
->setLabel('Card Number')
|
||||
->setDisableAutocomplete(true)
|
||||
->setSigil('number-input')
|
||||
->setError($e_card_number))
|
||||
->appendChild(
|
||||
id(new AphrontFormTextControl())
|
||||
->setLabel('CVC')
|
||||
->setDisableAutocomplete(true)
|
||||
->setSigil('cvc-input')
|
||||
->setError($e_card_cvc))
|
||||
->appendChild(
|
||||
id(new PhortuneMonthYearExpiryControl())
|
||||
->setLabel('Expiration')
|
||||
->setUser($user)
|
||||
->setError($e_card_exp))
|
||||
->appendChild(
|
||||
javelin_tag(
|
||||
'input',
|
||||
array(
|
||||
'hidden' => true,
|
||||
'name' => 'stripeToken',
|
||||
'sigil' => 'stripe-token-input',
|
||||
)))
|
||||
->appendChild(
|
||||
javelin_tag(
|
||||
'input',
|
||||
array(
|
||||
'hidden' => true,
|
||||
'name' => 'cardErrors',
|
||||
'sigil' => 'card-errors-input'
|
||||
)))
|
||||
->appendChild(
|
||||
phutil_tag(
|
||||
'input',
|
||||
array(
|
||||
'hidden' => true,
|
||||
'name' => 'stripeKey',
|
||||
'value' => $stripe_publishable_key,
|
||||
)))
|
||||
->appendChild(
|
||||
id(new AphrontFormSubmitControl())
|
||||
->setValue('Add Payment Method')
|
||||
->addCancelButton($account_uri));
|
||||
|
||||
Javelin::initBehavior(
|
||||
'stripe-payment-form',
|
||||
array(
|
||||
'stripePublishKey' => $stripe_publishable_key,
|
||||
'root' => $form_id,
|
||||
));
|
||||
|
||||
$title = pht('Add Payment Method');
|
||||
|
||||
$crumbs = $this->buildApplicationCrumbs();
|
||||
$crumbs->addCrumb(
|
||||
id(new PhabricatorCrumbView())
|
||||
->setName(pht('Account'))
|
||||
->setHref($account_uri));
|
||||
$crumbs->addCrumb(
|
||||
id(new PhabricatorCrumbView())
|
||||
->setName(pht('Payment Methods'))
|
||||
->setHref($request->getRequestURI()));
|
||||
|
||||
return
|
||||
$this->buildStandardPageResponse(
|
||||
array(
|
||||
$crumbs,
|
||||
$header,
|
||||
$errors,
|
||||
$form,
|
||||
),
|
||||
array(
|
||||
'title' => $title,
|
||||
'device' => true,
|
||||
'dust' => true,
|
||||
));
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 parseRawErrors($errors) {
|
||||
$card_number_error = null;
|
||||
$card_cvc_error = null;
|
||||
$card_expiration_error = null;
|
||||
$messages = array();
|
||||
foreach ($errors as $index => $error) {
|
||||
$type = key($error);
|
||||
$msg = reset($error);
|
||||
$messages[] = $msg;
|
||||
switch ($type) {
|
||||
case 'number':
|
||||
case 'invalid_number':
|
||||
case 'incorrect_number':
|
||||
$card_number_error = pht('Invalid');
|
||||
break;
|
||||
case 'cvc':
|
||||
case 'invalid_cvc':
|
||||
case 'incorrect_cvc':
|
||||
$card_cvc_error = pht('Invalid');
|
||||
break;
|
||||
case 'expiry':
|
||||
case 'invalid_expiry_month':
|
||||
case 'invalid_expiry_year':
|
||||
$card_expiration_error = pht('Invalid');
|
||||
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(
|
||||
'error_type: %s error_message: %s',
|
||||
$type,
|
||||
$msg);
|
||||
$this->logStripeError($error);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return array(
|
||||
$card_number_error,
|
||||
$card_cvc_error,
|
||||
$card_expiration_error,
|
||||
$messages
|
||||
);
|
||||
}
|
||||
|
||||
private function logStripeError($message) {
|
||||
phlog('STRIPE-ERROR '.$message);
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,25 @@
|
|||
<?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.')),
|
||||
);
|
||||
}
|
||||
|
||||
}
|
117
src/applications/phortune/query/PhortunePaymentMethodQuery.php
Normal file
117
src/applications/phortune/query/PhortunePaymentMethodQuery.php
Normal file
|
@ -0,0 +1,117 @@
|
|||
<?php
|
||||
|
||||
final class PhortunePaymentMethodQuery
|
||||
extends PhabricatorCursorPagedPolicyAwareQuery {
|
||||
|
||||
private $ids;
|
||||
private $phids;
|
||||
private $accountPHIDs;
|
||||
|
||||
const STATUS_ANY = 'status-any';
|
||||
const STATUS_OPEN = 'status-open';
|
||||
private $status = self::STATUS_ANY;
|
||||
|
||||
public function withIDs(array $ids) {
|
||||
$this->ids = $ids;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function withPHIDs(array $phids) {
|
||||
$this->phids = $phids;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function withAccountPHIDs(array $phids) {
|
||||
$this->accountPHIDs = $phids;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function withStatus($status) {
|
||||
$this->status = $status;
|
||||
return $this;
|
||||
}
|
||||
|
||||
protected function loadPage() {
|
||||
$table = new PhortunePaymentMethod();
|
||||
$conn = $table->establishConnection('r');
|
||||
|
||||
$rows = queryfx_all(
|
||||
$conn,
|
||||
'SELECT * FROM %T %Q %Q %Q',
|
||||
$table->getTableName(),
|
||||
$this->buildWhereClause($conn),
|
||||
$this->buildOrderClause($conn),
|
||||
$this->buildLimitClause($conn));
|
||||
|
||||
return $table->loadAllFromArray($rows);
|
||||
}
|
||||
|
||||
protected function willFilterPage(array $methods) {
|
||||
if (!$methods) {
|
||||
return array();
|
||||
}
|
||||
|
||||
$accounts = id(new PhortuneAccountQuery())
|
||||
->setViewer($this->getViewer())
|
||||
->withPHIDs(mpull($methods, 'getAccountPHID'))
|
||||
->execute();
|
||||
$accounts = mpull($accounts, null, 'getPHID');
|
||||
|
||||
foreach ($methods as $key => $method) {
|
||||
$account = idx($accounts, $method->getAccountPHID());
|
||||
if (!$account) {
|
||||
unset($methods[$key]);
|
||||
continue;
|
||||
}
|
||||
$method->attachAccount($account);
|
||||
}
|
||||
|
||||
return $methods;
|
||||
}
|
||||
|
||||
private function buildWhereClause(AphrontDatabaseConnection $conn) {
|
||||
$where = array();
|
||||
|
||||
if ($this->ids) {
|
||||
$where[] = qsprintf(
|
||||
$conn,
|
||||
'id IN (%Ld)',
|
||||
$this->ids);
|
||||
}
|
||||
|
||||
if ($this->phids) {
|
||||
$where[] = qsprintf(
|
||||
$conn,
|
||||
'phid IN (%Ls)',
|
||||
$this->phids);
|
||||
}
|
||||
|
||||
if ($this->accountPHIDs) {
|
||||
$where[] = qsprintf(
|
||||
$conn,
|
||||
'accountPHID IN (%Ls)',
|
||||
$this->accountPHIDs);
|
||||
}
|
||||
|
||||
switch ($this->status) {
|
||||
case self::STATUS_ANY;
|
||||
break;
|
||||
case self::STATUS_OPEN:
|
||||
$where[] = qsprintf(
|
||||
$conn,
|
||||
'status in (%Ls)',
|
||||
array(
|
||||
PhortunePaymentMethod::STATUS_ACTIVE,
|
||||
PhortunePaymentMethod::STATUS_FAILED,
|
||||
));
|
||||
break;
|
||||
default:
|
||||
throw new Exception("Unknown status '{$this->status}'!");
|
||||
}
|
||||
|
||||
$where[] = $this->buildPagingClause($conn);
|
||||
|
||||
return $this->formatWhereClause($where);
|
||||
}
|
||||
|
||||
}
|
|
@ -7,9 +7,15 @@
|
|||
final class PhortunePaymentMethod extends PhortuneDAO
|
||||
implements PhabricatorPolicyInterface {
|
||||
|
||||
const STATUS_ACTIVE = 'payment:active';
|
||||
const STATUS_FAILED = 'payment:failed';
|
||||
const STATUS_REMOVED = 'payment:removed';
|
||||
|
||||
protected $name;
|
||||
protected $status;
|
||||
protected $accountPHID;
|
||||
protected $authorPHID;
|
||||
protected $expiresEpoch;
|
||||
protected $metadata;
|
||||
|
||||
private $account;
|
||||
|
|
|
@ -1,17 +0,0 @@
|
|||
<?php
|
||||
|
||||
abstract class PhortuneStripeBaseController extends PhabricatorController {
|
||||
|
||||
public function buildStandardPageResponse($view, array $data) {
|
||||
$page = $this->buildStandardPageView();
|
||||
|
||||
$page->setApplicationName('Phortune - Stripe');
|
||||
$page->setBaseURI('/phortune/stripe/');
|
||||
$page->setTitle(idx($data, 'title'));
|
||||
$page->appendChild($view);
|
||||
|
||||
$response = new AphrontWebpageResponse();
|
||||
return $response->setContent($page->render());
|
||||
}
|
||||
|
||||
}
|
|
@ -1,146 +0,0 @@
|
|||
<?php
|
||||
|
||||
final class PhortuneStripeTestPaymentFormController
|
||||
extends PhortuneStripeBaseController {
|
||||
public function processRequest() {
|
||||
$request = $this->getRequest();
|
||||
$user = $request->getUser();
|
||||
$title = 'Test Payment Form';
|
||||
$error_view = null;
|
||||
$card_number_error = null;
|
||||
$card_cvc_error = null;
|
||||
$card_expiration_error = null;
|
||||
$stripe_key = $request->getStr('stripeKey');
|
||||
if (!$stripe_key) {
|
||||
$error_view = id(new AphrontErrorView())
|
||||
->setTitle('Missing stripeKey parameter in URI');
|
||||
}
|
||||
|
||||
if (!$error_view && $request->isFormPost()) {
|
||||
$card_errors = $request->getStr('cardErrors');
|
||||
$stripe_token = $request->getStr('stripeToken');
|
||||
if ($card_errors) {
|
||||
$raw_errors = json_decode($card_errors);
|
||||
list($card_number_error,
|
||||
$card_cvc_error,
|
||||
$card_expiration_error,
|
||||
$messages) = $this->parseRawErrors($raw_errors);
|
||||
$error_view = id(new AphrontErrorView())
|
||||
->setTitle('There were errors processing your card.')
|
||||
->setErrors($messages);
|
||||
} else if (!$stripe_token) {
|
||||
// this shouldn't happen, so show the user a very generic error
|
||||
// message and log that this error occurred...!
|
||||
$error_view = id(new AphrontErrorView())
|
||||
->setTitle('There was an unknown error processing your card.')
|
||||
->setErrors(array('Please try again.'));
|
||||
$error = 'payment form submitted but no stripe token and no errors';
|
||||
$this->logStripeError($error);
|
||||
} else {
|
||||
// success -- do something with $stripe_token!!
|
||||
}
|
||||
} else if (!$error_view) {
|
||||
$error_view = id(new AphrontErrorView())
|
||||
->setSeverity(AphrontErrorView::SEVERITY_NOTICE)
|
||||
->setTitle(
|
||||
'If you are using a test stripe key, use 4242424242424242, '.
|
||||
'any three digits for CVC, and any valid expiration date to '.
|
||||
'test!');
|
||||
}
|
||||
|
||||
$view = id(new AphrontPanelView())
|
||||
->setWidth(AphrontPanelView::WIDTH_FORM)
|
||||
->setHeader($title);
|
||||
|
||||
$form = id(new PhortuneStripePaymentFormView())
|
||||
->setUser($user)
|
||||
->setStripeKey($stripe_key)
|
||||
->setCardNumberError($card_number_error)
|
||||
->setCardCVCError($card_cvc_error)
|
||||
->setCardExpirationError($card_expiration_error);
|
||||
|
||||
$view->appendChild($form);
|
||||
|
||||
return
|
||||
$this->buildStandardPageResponse(
|
||||
array(
|
||||
$error_view,
|
||||
$view,
|
||||
),
|
||||
array(
|
||||
'title' => $title,
|
||||
));
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 parseRawErrors($errors) {
|
||||
$card_number_error = null;
|
||||
$card_cvc_error = null;
|
||||
$card_expiration_error = null;
|
||||
$messages = array();
|
||||
foreach ($errors as $index => $error) {
|
||||
$type = key($error);
|
||||
$msg = reset($error);
|
||||
$messages[] = $msg;
|
||||
switch ($type) {
|
||||
case 'number':
|
||||
case 'invalid_number':
|
||||
case 'incorrect_number':
|
||||
$card_number_error = true;
|
||||
break;
|
||||
case 'cvc':
|
||||
case 'invalid_cvc':
|
||||
case 'incorrect_cvc':
|
||||
$card_cvc_error = true;
|
||||
break;
|
||||
case 'expiry':
|
||||
case 'invalid_expiry_month':
|
||||
case 'invalid_expiry_year':
|
||||
$card_expiration_error = true;
|
||||
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(
|
||||
'error_type: %s error_message: %s',
|
||||
$type,
|
||||
$msg);
|
||||
$this->logStripeError($error);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// append a helpful "fix this" to the messages to be displayed to the user
|
||||
$messages[] = pht(
|
||||
'Please fix these errors and try again.',
|
||||
count($messages));
|
||||
|
||||
return array(
|
||||
$card_number_error,
|
||||
$card_cvc_error,
|
||||
$card_expiration_error,
|
||||
$messages
|
||||
);
|
||||
}
|
||||
|
||||
private function logStripeError($message) {
|
||||
phlog('STRIPE-ERROR '.$message);
|
||||
}
|
||||
|
||||
|
||||
}
|
|
@ -39,7 +39,7 @@ final class PhortuneStripePaymentFormView extends AphrontView {
|
|||
}
|
||||
|
||||
public function render() {
|
||||
$form_id = celerity_generate_unique_node_id();
|
||||
$form_id = celerity_generate_unique_node_id();
|
||||
require_celerity_resource('stripe-payment-form-css');
|
||||
require_celerity_resource('aphront-tooltip-css');
|
||||
Javelin::initBehavior('phabricator-tooltips');
|
||||
|
@ -105,7 +105,7 @@ final class PhortuneStripePaymentFormView extends AphrontView {
|
|||
)))
|
||||
->appendChild(
|
||||
id(new AphrontFormSubmitControl())
|
||||
->setValue('Submit Payment'));
|
||||
->setValue('Add Payment Method'));
|
||||
|
||||
Javelin::initBehavior(
|
||||
'stripe-payment-form',
|
|
@ -1206,6 +1206,10 @@ final class PhabricatorBuiltinPatchList extends PhabricatorSQLPatchList {
|
|||
'type' => 'sql',
|
||||
'name' => $this->getPatchPath('20130322.phortune.sql'),
|
||||
),
|
||||
'20130323.phortunepayment.sql' => array(
|
||||
'type' => 'sql',
|
||||
'name' => $this->getPatchPath('20130323.phortunepayment.sql'),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
@ -3,6 +3,7 @@
|
|||
* @requires javelin-behavior
|
||||
* javelin-dom
|
||||
* javelin-json
|
||||
* javelin-workflow
|
||||
* stripe-core
|
||||
*/
|
||||
|
||||
|
@ -73,8 +74,11 @@ JX.behavior('stripe-payment-form', function(config) {
|
|||
}
|
||||
if (errors.length != 0) {
|
||||
cardErrors.value = JX.JSON.stringify(errors);
|
||||
root.submit();
|
||||
return true;
|
||||
|
||||
JX.Workflow.newFromForm(root)
|
||||
.start();
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// no errors detected so contact Stripe asynchronously
|
||||
|
@ -110,14 +114,13 @@ JX.behavior('stripe-payment-form', function(config) {
|
|||
// success - we can use the token to create a customer object with
|
||||
// Stripe and let the billing commence!
|
||||
var token = response['id'];
|
||||
cardErrors.value = '[]';
|
||||
stripeToken.value = token;
|
||||
}
|
||||
root.submit();
|
||||
|
||||
JX.Workflow.newFromForm(root)
|
||||
.start();
|
||||
}
|
||||
|
||||
JX.DOM.listen(
|
||||
root,
|
||||
'submit',
|
||||
null,
|
||||
onsubmit);
|
||||
JX.DOM.listen(root, 'submit', null, onsubmit);
|
||||
});
|
||||
|
|
Loading…
Reference in a new issue