mirror of
https://we.phorge.it/source/phorge.git
synced 2025-01-22 12:41:19 +01:00
Add initial cut of PayPal and pay-once-at-checkout providers to Phortune
Summary: Paypal doesn't let us capture cards in a PCI-free way like Stripe and Balanced do, but we can provide a "pay with paypal" option at checkout. (For subscriptions, we'll have to invoice monthly to retain control over billing, but this doesn't seem wildly unreasonable.) The bitcoin provider MtGox works in a similar way, as do some other providers we might some day want to implement. This adds: - Hooks to providers so they can offer "pay once at checkout" workflows. - Hooks so providers can have controllers, for redirect-based third-party workflows. - Basic Paypal integration using the "Express Checkout Merchant API", which seems like the best fit for our use case. This only goes as far as shoving the user through the payment flow; we don't actually capture payments yet (paypal has around 35 different APIs, but this one seems to be the only PCI-free one which wouldn't give users an awful experience). This diff is fairly checkpointey, but Phortune doesn't really bill anything yet anyway. Ref T2787. Test Plan: Ran through Paypal sandbox workflow; "paid" for stuff. Reviewers: btrahan Reviewed By: btrahan CC: aran Maniphest Tasks: T2787 Differential Revision: https://secure.phabricator.com/D5834
This commit is contained in:
parent
67459092d7
commit
b51117790e
9 changed files with 369 additions and 16 deletions
|
@ -1604,6 +1604,7 @@ phutil_register_library_map(array(
|
|||
'PhortunePaymentMethodViewController' => 'applications/phortune/controller/PhortunePaymentMethodViewController.php',
|
||||
'PhortunePaymentProvider' => 'applications/phortune/provider/PhortunePaymentProvider.php',
|
||||
'PhortunePaymentProviderTestCase' => 'applications/phortune/provider/__tests__/PhortunePaymentProviderTestCase.php',
|
||||
'PhortunePaypalPaymentProvider' => 'applications/phortune/provider/PhortunePaypalPaymentProvider.php',
|
||||
'PhortuneProduct' => 'applications/phortune/storage/PhortuneProduct.php',
|
||||
'PhortuneProductEditController' => 'applications/phortune/controller/PhortuneProductEditController.php',
|
||||
'PhortuneProductEditor' => 'applications/phortune/editor/PhortuneProductEditor.php',
|
||||
|
@ -1612,6 +1613,7 @@ phutil_register_library_map(array(
|
|||
'PhortuneProductTransaction' => 'applications/phortune/storage/PhortuneProductTransaction.php',
|
||||
'PhortuneProductTransactionQuery' => 'applications/phortune/query/PhortuneProductTransactionQuery.php',
|
||||
'PhortuneProductViewController' => 'applications/phortune/controller/PhortuneProductViewController.php',
|
||||
'PhortuneProviderController' => 'applications/phortune/controller/PhortuneProviderController.php',
|
||||
'PhortunePurchase' => 'applications/phortune/storage/PhortunePurchase.php',
|
||||
'PhortuneStripePaymentProvider' => 'applications/phortune/provider/PhortuneStripePaymentProvider.php',
|
||||
'PhortuneTestExtraPaymentProvider' => 'applications/phortune/provider/__tests__/PhortuneTestExtraPaymentProvider.php',
|
||||
|
@ -3335,6 +3337,7 @@ phutil_register_library_map(array(
|
|||
'PhortunePaymentMethodQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
|
||||
'PhortunePaymentMethodViewController' => 'PhabricatorController',
|
||||
'PhortunePaymentProviderTestCase' => 'PhabricatorTestCase',
|
||||
'PhortunePaypalPaymentProvider' => 'PhortunePaymentProvider',
|
||||
'PhortuneProduct' =>
|
||||
array(
|
||||
0 => 'PhortuneDAO',
|
||||
|
@ -3347,6 +3350,7 @@ phutil_register_library_map(array(
|
|||
'PhortuneProductTransaction' => 'PhabricatorApplicationTransaction',
|
||||
'PhortuneProductTransactionQuery' => 'PhabricatorApplicationTransactionQuery',
|
||||
'PhortuneProductViewController' => 'PhortuneController',
|
||||
'PhortuneProviderController' => 'PhortuneController',
|
||||
'PhortunePurchase' => 'PhortuneDAO',
|
||||
'PhortuneStripePaymentProvider' => 'PhortunePaymentProvider',
|
||||
'PhortuneTestExtraPaymentProvider' => 'PhortunePaymentProvider',
|
||||
|
|
|
@ -49,6 +49,8 @@ final class PhabricatorApplicationPhortune extends PhabricatorApplication {
|
|||
'view/(?P<id>\d+)/' => 'PhortuneProductViewController',
|
||||
'edit/(?:(?P<id>\d+)/)?' => 'PhortuneProductEditController',
|
||||
),
|
||||
'provider/(?P<digest>[^/]+)/(?P<action>[^/]+)/'
|
||||
=> 'PhortuneProviderController',
|
||||
),
|
||||
);
|
||||
}
|
||||
|
|
|
@ -110,7 +110,7 @@ final class PhortuneAccountBuyController
|
|||
foreach ($methods as $method) {
|
||||
$method_control->addButton(
|
||||
$method->getID(),
|
||||
$method->getName(),
|
||||
$method->getBrand().' / '.$method->getLastFourDigits(),
|
||||
$method->getDescription());
|
||||
}
|
||||
}
|
||||
|
@ -118,28 +118,60 @@ final class PhortuneAccountBuyController
|
|||
$payment_method_uri = $this->getApplicationURI(
|
||||
$account->getID().'/paymentmethod/edit/');
|
||||
|
||||
$new_method = phutil_tag(
|
||||
'a',
|
||||
array(
|
||||
'href' => $payment_method_uri,
|
||||
'sigil' => 'workflow',
|
||||
),
|
||||
pht('Add New Payment Method'));
|
||||
|
||||
$form = id(new AphrontFormView())
|
||||
->setUser($user)
|
||||
->appendChild($method_control)
|
||||
->appendChild(
|
||||
->appendChild($method_control);
|
||||
|
||||
$add_providers = PhortunePaymentProvider::getProvidersForAddPaymentMethod();
|
||||
if ($add_providers) {
|
||||
$new_method = phutil_tag(
|
||||
'a',
|
||||
array(
|
||||
'class' => 'button grey',
|
||||
'href' => $payment_method_uri,
|
||||
'sigil' => 'workflow',
|
||||
),
|
||||
pht('Add New Payment Method'));
|
||||
$form->appendChild(
|
||||
id(new AphrontFormMarkupControl())
|
||||
->setValue($new_method))
|
||||
->appendChild(
|
||||
id(new AphrontFormSubmitControl())
|
||||
->setValue(pht("Dolla Dolla Bill Y'all")));
|
||||
->setValue($new_method));
|
||||
}
|
||||
|
||||
if ($methods || $add_providers) {
|
||||
$form
|
||||
->appendChild(
|
||||
id(new AphrontFormSubmitControl())
|
||||
->setValue(pht("Submit Payment"))
|
||||
->setDisabled(!$methods));
|
||||
}
|
||||
|
||||
$provider_form = null;
|
||||
|
||||
$pay_providers = PhortunePaymentProvider::getProvidersForOneTimePayment();
|
||||
if ($pay_providers) {
|
||||
$one_time_options = array();
|
||||
foreach ($pay_providers as $provider) {
|
||||
$one_time_options[] = $provider->renderOneTimePaymentButton(
|
||||
$account,
|
||||
$cart,
|
||||
$user);
|
||||
}
|
||||
|
||||
$provider_form = id(new AphrontFormLayoutView())
|
||||
->setPadded(true)
|
||||
->setBackgroundShading(true);
|
||||
$provider_form->appendChild(
|
||||
id(new AphrontFormMarkupControl())
|
||||
->setLabel('Pay With')
|
||||
->setValue($one_time_options));
|
||||
}
|
||||
|
||||
return $this->buildApplicationPage(
|
||||
array(
|
||||
$panel,
|
||||
$form,
|
||||
phutil_tag('br', array()),
|
||||
$provider_form,
|
||||
),
|
||||
array(
|
||||
'title' => $title,
|
||||
|
|
|
@ -0,0 +1,64 @@
|
|||
<?php
|
||||
|
||||
final class PhortuneProviderController extends PhortuneController {
|
||||
|
||||
private $digest;
|
||||
private $action;
|
||||
|
||||
public function willProcessRequest(array $data) {
|
||||
$this->digest = $data['digest'];
|
||||
$this->setAction($data['action']);
|
||||
}
|
||||
|
||||
public function setAction($action) {
|
||||
$this->action = $action;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getAction() {
|
||||
return $this->action;
|
||||
}
|
||||
|
||||
public function processRequest() {
|
||||
$request = $this->getRequest();
|
||||
$user = $request->getUser();
|
||||
|
||||
|
||||
// NOTE: This use of digests to identify payment providers is because
|
||||
// payment provider keys don't necessarily have restrictions on what they
|
||||
// contain (so they might have stuff that's not safe to put in URIs), and
|
||||
// using digests prevents errors with URI encoding.
|
||||
|
||||
$provider = PhortunePaymentProvider::getProviderByDigest($this->digest);
|
||||
if (!$provider) {
|
||||
throw new Exception("Invalid payment provider digest!");
|
||||
}
|
||||
|
||||
if (!$provider->canRespondToControllerAction($this->getAction())) {
|
||||
return new Aphront404Response();
|
||||
}
|
||||
|
||||
|
||||
$response = $provider->processControllerRequest($this, $request);
|
||||
|
||||
if ($response instanceof AphrontResponse) {
|
||||
return $response;
|
||||
}
|
||||
|
||||
$title = 'Phortune';
|
||||
|
||||
return $this->buildApplicationPage(
|
||||
$response,
|
||||
array(
|
||||
'title' => $title,
|
||||
'device' => true,
|
||||
'dust' => true,
|
||||
));
|
||||
}
|
||||
|
||||
|
||||
public function loadCart($id) {
|
||||
return id(new PhortuneCart());
|
||||
}
|
||||
|
||||
}
|
|
@ -38,8 +38,19 @@ final class PhabricatorPhortuneConfigOptions
|
|||
"NOTE: Enabling this provider gives all users infinite free ".
|
||||
"money! You should enable it **ONLY** for testing and ".
|
||||
"development."))
|
||||
->setLocked(true),
|
||||
$this->newOption('phortune.paypal.api-username', 'string', null)
|
||||
->setLocked(true)
|
||||
|
||||
->setDescription(
|
||||
pht('PayPal API username.')),
|
||||
$this->newOption('phortune.paypal.api-password', 'string', null)
|
||||
->setHidden(true)
|
||||
->setDescription(
|
||||
pht('PayPal API password.')),
|
||||
$this->newOption('phortune.paypal.api-signature', 'string', null)
|
||||
->setHidden(true)
|
||||
->setDescription(
|
||||
pht('PayPal API signature.')),
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
@ -37,6 +37,27 @@ abstract class PhortunePaymentProvider {
|
|||
return $providers;
|
||||
}
|
||||
|
||||
public static function getProvidersForOneTimePayment() {
|
||||
$providers = self::getEnabledProviders();
|
||||
foreach ($providers as $key => $provider) {
|
||||
if (!$provider->canProcessOneTimePayments()) {
|
||||
unset($providers[$key]);
|
||||
}
|
||||
}
|
||||
return $providers;
|
||||
}
|
||||
|
||||
public static function getProviderByDigest($digest) {
|
||||
$providers = self::getEnabledProviders();
|
||||
foreach ($providers as $key => $provider) {
|
||||
$provider_digest = PhabricatorHash::digestForIndex($key);
|
||||
if ($provider_digest == $digest) {
|
||||
return $provider;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
abstract public function isEnabled();
|
||||
|
||||
final public function getProviderKey() {
|
||||
|
@ -137,4 +158,47 @@ abstract class PhortunePaymentProvider {
|
|||
}
|
||||
|
||||
|
||||
/* -( One-Time Payments )-------------------------------------------------- */
|
||||
|
||||
|
||||
public function canProcessOneTimePayments() {
|
||||
return false;
|
||||
}
|
||||
|
||||
public function renderOneTimePaymentButton(
|
||||
PhortuneAccount $account,
|
||||
PhortuneCart $cart,
|
||||
PhabricatorUser $user) {
|
||||
throw new PhortuneNotImplementedException($this);
|
||||
}
|
||||
|
||||
|
||||
/* -( Controllers )-------------------------------------------------------- */
|
||||
|
||||
|
||||
final public function getControllerURI(
|
||||
$action,
|
||||
array $params = array()) {
|
||||
|
||||
$digest = PhabricatorHash::digestForIndex($this->getProviderKey());
|
||||
|
||||
$app = PhabricatorApplication::getByClass('PhabricatorApplicationPhortune');
|
||||
$path = $app->getBaseURI().'provider/'.$digest.'/'.$action.'/';
|
||||
|
||||
$uri = new PhutilURI($path);
|
||||
$uri->setQueryParams($params);
|
||||
|
||||
return PhabricatorEnv::getURI((string)$uri);
|
||||
}
|
||||
|
||||
public function canRespondToControllerAction($action) {
|
||||
return false;
|
||||
}
|
||||
|
||||
public function processControllerRequest(
|
||||
PhortuneProviderController $controller,
|
||||
AphrontRequest $request) {
|
||||
throw new PhortuneNotImplementedException($this);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -0,0 +1,168 @@
|
|||
<?php
|
||||
|
||||
final class PhortunePaypalPaymentProvider extends PhortunePaymentProvider {
|
||||
|
||||
public function isEnabled() {
|
||||
return $this->getPaypalAPIUsername() &&
|
||||
$this->getPaypalAPIPassword() &&
|
||||
$this->getPaypalAPISignature();
|
||||
}
|
||||
|
||||
public function getProviderType() {
|
||||
return 'paypal';
|
||||
}
|
||||
|
||||
public function getProviderDomain() {
|
||||
return 'paypal.com';
|
||||
}
|
||||
|
||||
public function getPaymentMethodDescription() {
|
||||
return 'Paypal Account';
|
||||
}
|
||||
|
||||
public function getPaymentMethodIcon() {
|
||||
return 'rsrc/phortune/paypal.png';
|
||||
}
|
||||
|
||||
public function getPaymentMethodProviderDescription() {
|
||||
return "Paypal";
|
||||
}
|
||||
|
||||
|
||||
public function canHandlePaymentMethod(PhortunePaymentMethod $method) {
|
||||
$type = $method->getMetadataValue('type');
|
||||
return ($type == 'paypal');
|
||||
}
|
||||
|
||||
protected function executeCharge(
|
||||
PhortunePaymentMethod $payment_method,
|
||||
PhortuneCharge $charge) {
|
||||
throw new Exception("!");
|
||||
}
|
||||
|
||||
private function getPaypalAPIUsername() {
|
||||
return PhabricatorEnv::getEnvConfig('phortune.paypal.api-username');
|
||||
}
|
||||
|
||||
private function getPaypalAPIPassword() {
|
||||
return PhabricatorEnv::getEnvConfig('phortune.paypal.api-password');
|
||||
}
|
||||
|
||||
private function getPaypalAPISignature() {
|
||||
return PhabricatorEnv::getEnvConfig('phortune.paypal.api-signature');
|
||||
}
|
||||
|
||||
/* -( One-Time Payments )-------------------------------------------------- */
|
||||
|
||||
public function canProcessOneTimePayments() {
|
||||
return true;
|
||||
}
|
||||
|
||||
public function renderOneTimePaymentButton(
|
||||
PhortuneAccount $account,
|
||||
PhortuneCart $cart,
|
||||
PhabricatorUser $user) {
|
||||
|
||||
$uri = $this->getControllerURI(
|
||||
'checkout',
|
||||
array(
|
||||
'cartID' => $cart->getID(),
|
||||
));
|
||||
|
||||
return phabricator_form(
|
||||
$user,
|
||||
array(
|
||||
'action' => $uri,
|
||||
'method' => 'POST',
|
||||
),
|
||||
phutil_tag(
|
||||
'button',
|
||||
array(
|
||||
'class' => 'green',
|
||||
'type' => 'submit',
|
||||
),
|
||||
pht('Pay with Paypal')));
|
||||
}
|
||||
|
||||
|
||||
/* -( Controllers )-------------------------------------------------------- */
|
||||
|
||||
|
||||
public function canRespondToControllerAction($action) {
|
||||
switch ($action) {
|
||||
case 'checkout':
|
||||
case 'charge':
|
||||
case 'cancel':
|
||||
return true;
|
||||
}
|
||||
return parent::canRespondToControllerAction();
|
||||
}
|
||||
|
||||
public function processControllerRequest(
|
||||
PhortuneProviderController $controller,
|
||||
AphrontRequest $request) {
|
||||
|
||||
$cart = $controller->loadCart($request->getInt('cartID'));
|
||||
if (!$cart) {
|
||||
return new Aphront404Response();
|
||||
}
|
||||
|
||||
switch ($controller->getAction()) {
|
||||
case 'checkout':
|
||||
$return_uri = $this->getControllerURI(
|
||||
'charge',
|
||||
array(
|
||||
'cartID' => $cart->getID(),
|
||||
));
|
||||
|
||||
$cancel_uri = $this->getControllerURI(
|
||||
'cancel',
|
||||
array(
|
||||
'cartID' => $cart->getID(),
|
||||
));
|
||||
|
||||
$total_in_cents = $cart->getTotalInCents();
|
||||
$price = PhortuneUtil::formatBareCurrency($total_in_cents);
|
||||
|
||||
$result = $this
|
||||
->newPaypalAPICall()
|
||||
->setRawPayPalQuery(
|
||||
'SetExpressCheckout',
|
||||
array(
|
||||
'PAYMENTREQUEST_0_AMT' => $price,
|
||||
'PAYMENTREQUEST_0_CURRENCYCODE' => 'USD',
|
||||
'RETURNURL' => $return_uri,
|
||||
'CANCELURL' => $cancel_uri,
|
||||
'PAYMENTREQUEST_0_PAYMENTACTION' => 'Sale',
|
||||
))
|
||||
->resolve();
|
||||
|
||||
$uri = new PhutilURI('https://www.sandbox.paypal.com/cgi-bin/webscr');
|
||||
$uri->setQueryParams(
|
||||
array(
|
||||
'cmd' => '_express-checkout',
|
||||
'token' => $result['TOKEN'],
|
||||
));
|
||||
|
||||
return id(new AphrontRedirectResponse())->setURI($uri);
|
||||
case 'charge':
|
||||
var_dump($_REQUEST);
|
||||
break;
|
||||
case 'cancel':
|
||||
var_dump($_REQUEST);
|
||||
break;
|
||||
}
|
||||
|
||||
throw new Exception("The rest of this isn't implemented yet.");
|
||||
}
|
||||
|
||||
private function newPaypalAPICall() {
|
||||
return id(new PhutilPayPalAPIFuture())
|
||||
->setHost('https://api-3t.sandbox.paypal.com/nvp')
|
||||
->setAPIUsername($this->getPaypalAPIUsername())
|
||||
->setAPIPassword($this->getPaypalAPIPassword())
|
||||
->setAPISignature($this->getPaypalAPISignature());
|
||||
}
|
||||
|
||||
|
||||
}
|
|
@ -28,6 +28,10 @@ final class PhortuneCart extends PhortuneDAO {
|
|||
return $this;
|
||||
}
|
||||
|
||||
public function getTotalInCents() {
|
||||
return 123;
|
||||
}
|
||||
|
||||
public function getPurchases() {
|
||||
if ($this->purchases === null) {
|
||||
throw new Exception("Purchases not attached to cart!");
|
||||
|
|
|
@ -31,4 +31,8 @@ final class PhortuneUtil {
|
|||
return $display_value;
|
||||
}
|
||||
|
||||
public static function formatBareCurrency($price_in_cents) {
|
||||
return str_replace('$', '', self::formatCurrency($price_in_cents));
|
||||
}
|
||||
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue