diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php index f798af18ac..db3c0f7613 100644 --- a/src/__phutil_library_map__.php +++ b/src/__phutil_library_map__.php @@ -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', diff --git a/src/applications/phortune/application/PhabricatorApplicationPhortune.php b/src/applications/phortune/application/PhabricatorApplicationPhortune.php index 5440cda2aa..fe08ef4487 100644 --- a/src/applications/phortune/application/PhabricatorApplicationPhortune.php +++ b/src/applications/phortune/application/PhabricatorApplicationPhortune.php @@ -49,6 +49,8 @@ final class PhabricatorApplicationPhortune extends PhabricatorApplication { 'view/(?P\d+)/' => 'PhortuneProductViewController', 'edit/(?:(?P\d+)/)?' => 'PhortuneProductEditController', ), + 'provider/(?P[^/]+)/(?P[^/]+)/' + => 'PhortuneProviderController', ), ); } diff --git a/src/applications/phortune/controller/PhortuneAccountBuyController.php b/src/applications/phortune/controller/PhortuneAccountBuyController.php index 10158a78e7..6f21876e1e 100644 --- a/src/applications/phortune/controller/PhortuneAccountBuyController.php +++ b/src/applications/phortune/controller/PhortuneAccountBuyController.php @@ -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, diff --git a/src/applications/phortune/controller/PhortuneProviderController.php b/src/applications/phortune/controller/PhortuneProviderController.php new file mode 100644 index 0000000000..15d56f485b --- /dev/null +++ b/src/applications/phortune/controller/PhortuneProviderController.php @@ -0,0 +1,64 @@ +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()); + } + +} diff --git a/src/applications/phortune/option/PhabricatorPhortuneConfigOptions.php b/src/applications/phortune/option/PhabricatorPhortuneConfigOptions.php index fcecd06952..6910e0b890 100644 --- a/src/applications/phortune/option/PhabricatorPhortuneConfigOptions.php +++ b/src/applications/phortune/option/PhabricatorPhortuneConfigOptions.php @@ -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.')), ); } diff --git a/src/applications/phortune/provider/PhortunePaymentProvider.php b/src/applications/phortune/provider/PhortunePaymentProvider.php index c0ca9cd492..dd5eeb3614 100644 --- a/src/applications/phortune/provider/PhortunePaymentProvider.php +++ b/src/applications/phortune/provider/PhortunePaymentProvider.php @@ -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); + } + } diff --git a/src/applications/phortune/provider/PhortunePaypalPaymentProvider.php b/src/applications/phortune/provider/PhortunePaypalPaymentProvider.php new file mode 100644 index 0000000000..321305602b --- /dev/null +++ b/src/applications/phortune/provider/PhortunePaypalPaymentProvider.php @@ -0,0 +1,168 @@ +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()); + } + + +} diff --git a/src/applications/phortune/storage/PhortuneCart.php b/src/applications/phortune/storage/PhortuneCart.php index 48fca94565..3c4238a62e 100644 --- a/src/applications/phortune/storage/PhortuneCart.php +++ b/src/applications/phortune/storage/PhortuneCart.php @@ -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!"); diff --git a/src/applications/phortune/util/PhortuneUtil.php b/src/applications/phortune/util/PhortuneUtil.php index 8ea076b5a1..57709ab927 100644 --- a/src/applications/phortune/util/PhortuneUtil.php +++ b/src/applications/phortune/util/PhortuneUtil.php @@ -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)); + } + }