Page Menu
Home
Phorge
Search
Configure Global Search
Log In
Files
F2896586
No One
Temporary
Actions
View File
Edit File
Delete File
View Transforms
Subscribe
Award Token
Flag For Later
Advanced/Developer...
View Handle
View Hovercard
Size
14 KB
Referenced Files
None
Subscribers
None
View Options
diff --git a/src/applications/phortune/provider/PhortuneStripePaymentProvider.php b/src/applications/phortune/provider/PhortuneStripePaymentProvider.php
index efac0a652d..5a19c928d8 100644
--- a/src/applications/phortune/provider/PhortuneStripePaymentProvider.php
+++ b/src/applications/phortune/provider/PhortuneStripePaymentProvider.php
@@ -1,236 +1,244 @@
<?php
final class PhortuneStripePaymentProvider extends PhortunePaymentProvider {
public function isEnabled() {
return $this->getPublishableKey() &&
$this->getSecretKey();
}
public function getProviderType() {
return 'stripe';
}
public function getProviderDomain() {
return 'stripe.com';
}
public function getPaymentMethodDescription() {
return pht('Add Credit or Debit Card (US and Canada)');
}
public function getPaymentMethodIcon() {
return celerity_get_resource_uri('/rsrc/image/phortune/stripe.png');
}
public function getPaymentMethodProviderDescription() {
return pht('Processed by Stripe');
}
public function getDefaultPaymentMethodDisplayName(
PhortunePaymentMethod $method) {
return pht('Credit/Debit Card');
}
public function canHandlePaymentMethod(PhortunePaymentMethod $method) {
$type = $method->getMetadataValue('type');
return ($type === 'stripe.customer');
}
/**
* @phutil-external-symbol class Stripe_Charge
+ * @phutil-external-symbol class Stripe_CardError
*/
protected function executeCharge(
PhortunePaymentMethod $method,
PhortuneCharge $charge) {
$root = dirname(phutil_get_library_root('phabricator'));
require_once $root.'/externals/stripe-php/lib/Stripe.php';
$price = $charge->getAmountAsCurrency();
$secret_key = $this->getSecretKey();
$params = array(
'amount' => $price->getValue(),
'currency' => $price->getCurrency(),
'customer' => $method->getMetadataValue('stripe.customerID'),
'description' => $charge->getPHID(),
'capture' => true,
);
- $stripe_charge = Stripe_Charge::create($params, $secret_key);
+ try {
+ $stripe_charge = Stripe_Charge::create($params, $secret_key);
+ } catch (Stripe_CardError $ex) {
+ // TODO: Fail charge explicitly.
+ throw $ex;
+ }
+
$id = $stripe_charge->id;
if (!$id) {
throw new Exception('Stripe charge call did not return an ID!');
}
$charge->setMetadataValue('stripe.chargeID', $id);
+ $charge->save();
}
private function getPublishableKey() {
return PhabricatorEnv::getEnvConfig('phortune.stripe.publishable-key');
}
private function getSecretKey() {
return PhabricatorEnv::getEnvConfig('phortune.stripe.secret-key');
}
/* -( Adding Payment Methods )--------------------------------------------- */
public function canCreatePaymentMethods() {
return true;
}
/**
* @phutil-external-symbol class Stripe_Token
* @phutil-external-symbol class Stripe_Customer
*/
public function createPaymentMethodFromRequest(
AphrontRequest $request,
PhortunePaymentMethod $method,
array $token) {
$errors = array();
$root = dirname(phutil_get_library_root('phabricator'));
require_once $root.'/externals/stripe-php/lib/Stripe.php';
$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);
$account_phid = $method->getAccountPHID();
$author_phid = $method->getAuthorPHID();
$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);
$card = $info->card;
$method
->setBrand($card->brand)
->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;
}
public function renderCreatePaymentMethodForm(
AphrontRequest $request,
array $errors) {
$ccform = id(new PhortuneCreditCardForm())
->setUser($request->getUser())
->setErrors($errors)
->addScript('https://js.stripe.com/v2/');
Javelin::initBehavior(
'stripe-payment-form',
array(
'stripePublishableKey' => $this->getPublishableKey(),
'formID' => $ccform->getFormID(),
));
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;
}
/**
* 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;
}
}
diff --git a/src/applications/phortune/provider/PhortuneWePayPaymentProvider.php b/src/applications/phortune/provider/PhortuneWePayPaymentProvider.php
index 0c11e4186b..0bfe334654 100644
--- a/src/applications/phortune/provider/PhortuneWePayPaymentProvider.php
+++ b/src/applications/phortune/provider/PhortuneWePayPaymentProvider.php
@@ -1,221 +1,222 @@
<?php
final class PhortuneWePayPaymentProvider extends PhortunePaymentProvider {
public function isEnabled() {
return $this->getWePayClientID() &&
$this->getWePayClientSecret() &&
$this->getWePayAccessToken() &&
$this->getWePayAccountID();
}
public function getProviderType() {
return 'wepay';
}
public function getProviderDomain() {
return 'wepay.com';
}
public function getPaymentMethodDescription() {
return pht('Credit Card or Bank Account');
}
public function getPaymentMethodIcon() {
return celerity_get_resource_uri('/rsrc/image/phortune/wepay.png');
}
public function getPaymentMethodProviderDescription() {
return 'WePay';
}
public function canHandlePaymentMethod(PhortunePaymentMethod $method) {
$type = $method->getMetadataValue('type');
return ($type == 'wepay');
}
protected function executeCharge(
PhortunePaymentMethod $payment_method,
PhortuneCharge $charge) {
throw new Exception('!');
}
private function getWePayClientID() {
return PhabricatorEnv::getEnvConfig('phortune.wepay.client-id');
}
private function getWePayClientSecret() {
return PhabricatorEnv::getEnvConfig('phortune.wepay.client-secret');
}
private function getWePayAccessToken() {
return PhabricatorEnv::getEnvConfig('phortune.wepay.access-token');
}
private function getWePayAccountID() {
return PhabricatorEnv::getEnvConfig('phortune.wepay.account-id');
}
/* -( One-Time Payments )-------------------------------------------------- */
public function canProcessOneTimePayments() {
return true;
}
/* -( Controllers )-------------------------------------------------------- */
public function canRespondToControllerAction($action) {
switch ($action) {
case 'checkout':
case 'charge':
case 'cancel':
return true;
}
return parent::canRespondToControllerAction();
}
/**
* @phutil-external-symbol class WePay
*/
public function processControllerRequest(
PhortuneProviderController $controller,
AphrontRequest $request) {
$viewer = $request->getUser();
$cart = $controller->loadCart($request->getInt('cartID'));
if (!$cart) {
return new Aphront404Response();
}
$root = dirname(phutil_get_library_root('phabricator'));
require_once $root.'/externals/wepay/wepay.php';
WePay::useStaging(
$this->getWePayClientID(),
$this->getWePayClientSecret());
$wepay = new WePay($this->getWePayAccessToken());
$charge = id(new PhortuneChargeQuery())
->setViewer($viewer)
->withCartPHIDs(array($cart->getPHID()))
->withStatuses(
array(
PhortuneCharge::STATUS_CHARGING,
))
->executeOne();
switch ($controller->getAction()) {
case 'checkout':
if ($charge) {
throw new Exception(pht('Cart is already charging!'));
}
break;
case 'charge':
case 'cancel':
if (!$charge) {
throw new Exception(pht('Cart is not charging yet!'));
}
break;
}
switch ($controller->getAction()) {
case 'checkout':
$return_uri = $this->getControllerURI(
'charge',
array(
'cartID' => $cart->getID(),
));
$cancel_uri = $this->getControllerURI(
'cancel',
array(
'cartID' => $cart->getID(),
));
$price = $cart->getTotalPriceAsCurrency();
$params = array(
'account_id' => $this->getWePayAccountID(),
'short_description' => 'Services', // TODO
'type' => 'SERVICE',
'amount' => $price->formatBareValue(),
'long_description' => 'Services', // TODO
'reference_id' => $cart->getPHID(),
'app_fee' => 0,
'fee_payer' => 'Payee',
'redirect_uri' => $return_uri,
'fallback_uri' => $cancel_uri,
// NOTE: If we don't `auto_capture`, we might get a result back in
// either an "authorized" or a "reserved" state. We can't capture
// an "authorized" result, so just autocapture.
'auto_capture' => true,
'require_shipping' => 0,
'shipping_fee' => 0,
'charge_tax' => 0,
'mode' => 'regular',
'funding_sources' => 'bank,cc'
);
- $cart->willApplyCharge($viewer, $this);
-
+ $charge = $cart->willApplyCharge($viewer, $this);
$result = $wepay->request('checkout/create', $params);
$cart->setMetadataValue('provider.checkoutURI', $result->checkout_uri);
- $cart->setMetadataValue('wepay.checkoutID', $result->checkout_id);
$cart->save();
+ $charge->setMetadataValue('wepay.checkoutID', $result->checkout_id);
+ $charge->save();
+
$uri = new PhutilURI($result->checkout_uri);
return id(new AphrontRedirectResponse())
->setIsExternal(true)
->setURI($uri);
case 'charge':
$checkout_id = $request->getInt('checkout_id');
$params = array(
'checkout_id' => $checkout_id,
);
$checkout = $wepay->request('checkout', $params);
if ($checkout->reference_id != $cart->getPHID()) {
throw new Exception(
pht('Checkout reference ID does not match cart PHID!'));
}
switch ($checkout->state) {
case 'authorized':
case 'reserved':
case 'captured':
break;
default:
throw new Exception(
pht(
'Checkout is in bad state "%s"!',
$result->state));
}
$unguarded = AphrontWriteGuard::beginScopedUnguardedWrites();
$cart->didApplyCharge($charge);
unset($unguarded);
return id(new AphrontRedirectResponse())
->setURI($cart->getDoneURI());
case 'cancel':
// TODO: I don't know how it's possible to cancel out of a WePay
// charge workflow.
throw new Exception(
pht('How did you get here? WePay has no cancel flow in its UI...?'));
break;
}
throw new Exception(
pht('Unsupported action "%s".', $controller->getAction()));
}
}
File Metadata
Details
Attached
Mime Type
text/x-diff
Expires
Jan 19 2025, 23:18 (6 w, 4 d ago)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
1129810
Default Alt Text
(14 KB)
Attached To
Mode
rP Phorge
Attached
Detach File
Event Timeline
Log In to Comment