diff --git a/resources/sql/autopatches/20141007.phortuneprovider.sql b/resources/sql/autopatches/20141007.phortuneprovider.sql new file mode 100644 index 0000000000..c8f47f7485 --- /dev/null +++ b/resources/sql/autopatches/20141007.phortuneprovider.sql @@ -0,0 +1,12 @@ +CREATE TABLE {$NAMESPACE}_phortune.phortune_paymentproviderconfig ( + id INT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY, + phid VARCHAR(64) NOT NULL COLLATE utf8_bin, + merchantPHID VARCHAR(64) NOT NULL COLLATE utf8_bin, + providerClassKey BINARY(12) NOT NULL, + providerClass VARCHAR(128) NOT NULL COLLATE utf8_bin, + metadata LONGTEXT NOT NULL COLLATE utf8_bin, + dateCreated INT UNSIGNED NOT NULL, + dateModified INT UNSIGNED NOT NULL, + UNIQUE KEY `key_phid` (phid), + UNIQUE KEY `key_merchant` (merchantPHID, providerClassKey) +) ENGINE=InnoDB, COLLATE=utf8_bin; diff --git a/resources/sql/autopatches/20141007.phortuneproviderx.sql b/resources/sql/autopatches/20141007.phortuneproviderx.sql new file mode 100644 index 0000000000..0bc1f72896 --- /dev/null +++ b/resources/sql/autopatches/20141007.phortuneproviderx.sql @@ -0,0 +1,19 @@ +CREATE TABLE {$NAMESPACE}_phortune.phortune_paymentproviderconfigtransaction ( + id INT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY, + phid VARCHAR(64) COLLATE utf8_bin NOT NULL, + authorPHID VARCHAR(64) COLLATE utf8_bin NOT NULL, + objectPHID VARCHAR(64) COLLATE utf8_bin NOT NULL, + viewPolicy VARCHAR(64) COLLATE utf8_bin NOT NULL, + editPolicy VARCHAR(64) COLLATE utf8_bin NOT NULL, + commentPHID VARCHAR(64) COLLATE utf8_bin DEFAULT NULL, + commentVersion INT UNSIGNED NOT NULL, + transactionType VARCHAR(32) COLLATE utf8_bin NOT NULL, + oldValue LONGTEXT COLLATE utf8_bin NOT NULL, + newValue LONGTEXT COLLATE utf8_bin NOT NULL, + contentSource LONGTEXT COLLATE utf8_bin NOT NULL, + metadata LONGTEXT COLLATE utf8_bin NOT NULL, + dateCreated INT UNSIGNED NOT NULL, + dateModified INT UNSIGNED NOT NULL, + UNIQUE KEY `key_phid` (`phid`), + KEY `key_object` (`objectPHID`) +) ENGINE=InnoDB, COLLATE utf8_general_ci; diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php index 83a7367465..ffba1823f7 100644 --- a/src/__phutil_library_map__.php +++ b/src/__phutil_library_map__.php @@ -1966,7 +1966,6 @@ phutil_register_library_map(array( 'PhabricatorPholioConfigOptions' => 'applications/pholio/config/PhabricatorPholioConfigOptions.php', 'PhabricatorPholioMockTestDataGenerator' => 'applications/pholio/lipsum/PhabricatorPholioMockTestDataGenerator.php', 'PhabricatorPhortuneApplication' => 'applications/phortune/application/PhabricatorPhortuneApplication.php', - 'PhabricatorPhortuneConfigOptions' => 'applications/phortune/option/PhabricatorPhortuneConfigOptions.php', 'PhabricatorPhragmentApplication' => 'applications/phragment/application/PhabricatorPhragmentApplication.php', 'PhabricatorPhrequentApplication' => 'applications/phrequent/application/PhabricatorPhrequentApplication.php', 'PhabricatorPhrequentConfigOptions' => 'applications/phrequent/config/PhabricatorPhrequentConfigOptions.php', @@ -2589,6 +2588,7 @@ phutil_register_library_map(array( 'PhortuneMultiplePaymentProvidersException' => 'applications/phortune/exception/PhortuneMultiplePaymentProvidersException.php', 'PhortuneNoPaymentProviderException' => 'applications/phortune/exception/PhortuneNoPaymentProviderException.php', 'PhortuneNotImplementedException' => 'applications/phortune/exception/PhortuneNotImplementedException.php', + 'PhortunePayPalPaymentProvider' => 'applications/phortune/provider/PhortunePayPalPaymentProvider.php', 'PhortunePaymentMethod' => 'applications/phortune/storage/PhortunePaymentMethod.php', 'PhortunePaymentMethodCreateController' => 'applications/phortune/controller/PhortunePaymentMethodCreateController.php', 'PhortunePaymentMethodDisableController' => 'applications/phortune/controller/PhortunePaymentMethodDisableController.php', @@ -2596,22 +2596,26 @@ phutil_register_library_map(array( 'PhortunePaymentMethodPHIDType' => 'applications/phortune/phid/PhortunePaymentMethodPHIDType.php', 'PhortunePaymentMethodQuery' => 'applications/phortune/query/PhortunePaymentMethodQuery.php', 'PhortunePaymentProvider' => 'applications/phortune/provider/PhortunePaymentProvider.php', - 'PhortunePaymentProviderTestCase' => 'applications/phortune/provider/__tests__/PhortunePaymentProviderTestCase.php', - 'PhortunePaypalPaymentProvider' => 'applications/phortune/provider/PhortunePaypalPaymentProvider.php', + 'PhortunePaymentProviderConfig' => 'applications/phortune/storage/PhortunePaymentProviderConfig.php', + 'PhortunePaymentProviderConfigEditor' => 'applications/phortune/editor/PhortunePaymentProviderConfigEditor.php', + 'PhortunePaymentProviderConfigQuery' => 'applications/phortune/query/PhortunePaymentProviderConfigQuery.php', + 'PhortunePaymentProviderConfigTransaction' => 'applications/phortune/storage/PhortunePaymentProviderConfigTransaction.php', + 'PhortunePaymentProviderConfigTransactionQuery' => 'applications/phortune/query/PhortunePaymentProviderConfigTransactionQuery.php', + 'PhortunePaymentProviderPHIDType' => 'applications/phortune/phid/PhortunePaymentProviderPHIDType.php', 'PhortuneProduct' => 'applications/phortune/storage/PhortuneProduct.php', 'PhortuneProductImplementation' => 'applications/phortune/product/PhortuneProductImplementation.php', 'PhortuneProductListController' => 'applications/phortune/controller/PhortuneProductListController.php', 'PhortuneProductPHIDType' => 'applications/phortune/phid/PhortuneProductPHIDType.php', 'PhortuneProductQuery' => 'applications/phortune/query/PhortuneProductQuery.php', 'PhortuneProductViewController' => 'applications/phortune/controller/PhortuneProductViewController.php', - 'PhortuneProviderController' => 'applications/phortune/controller/PhortuneProviderController.php', + 'PhortuneProviderActionController' => 'applications/phortune/controller/PhortuneProviderActionController.php', + 'PhortuneProviderEditController' => 'applications/phortune/controller/PhortuneProviderEditController.php', 'PhortunePurchase' => 'applications/phortune/storage/PhortunePurchase.php', 'PhortunePurchasePHIDType' => 'applications/phortune/phid/PhortunePurchasePHIDType.php', 'PhortunePurchaseQuery' => 'applications/phortune/query/PhortunePurchaseQuery.php', 'PhortunePurchaseViewController' => 'applications/phortune/controller/PhortunePurchaseViewController.php', 'PhortuneSchemaSpec' => 'applications/phortune/storage/PhortuneSchemaSpec.php', 'PhortuneStripePaymentProvider' => 'applications/phortune/provider/PhortuneStripePaymentProvider.php', - 'PhortuneTestExtraPaymentProvider' => 'applications/phortune/provider/__tests__/PhortuneTestExtraPaymentProvider.php', 'PhortuneTestPaymentProvider' => 'applications/phortune/provider/PhortuneTestPaymentProvider.php', 'PhortuneWePayPaymentProvider' => 'applications/phortune/provider/PhortuneWePayPaymentProvider.php', 'PhragmentBrowseController' => 'applications/phragment/controller/PhragmentBrowseController.php', @@ -4935,7 +4939,6 @@ phutil_register_library_map(array( 'PhabricatorPholioConfigOptions' => 'PhabricatorApplicationConfigOptions', 'PhabricatorPholioMockTestDataGenerator' => 'PhabricatorTestDataGenerator', 'PhabricatorPhortuneApplication' => 'PhabricatorApplication', - 'PhabricatorPhortuneConfigOptions' => 'PhabricatorApplicationConfigOptions', 'PhabricatorPhragmentApplication' => 'PhabricatorApplication', 'PhabricatorPhrequentApplication' => 'PhabricatorApplication', 'PhabricatorPhrequentConfigOptions' => 'PhabricatorApplicationConfigOptions', @@ -5639,6 +5642,7 @@ phutil_register_library_map(array( 'PhortuneMultiplePaymentProvidersException' => 'Exception', 'PhortuneNoPaymentProviderException' => 'Exception', 'PhortuneNotImplementedException' => 'Exception', + 'PhortunePayPalPaymentProvider' => 'PhortunePaymentProvider', 'PhortunePaymentMethod' => array( 'PhortuneDAO', 'PhabricatorPolicyInterface', @@ -5648,8 +5652,15 @@ phutil_register_library_map(array( 'PhortunePaymentMethodEditController' => 'PhortuneController', 'PhortunePaymentMethodPHIDType' => 'PhabricatorPHIDType', 'PhortunePaymentMethodQuery' => 'PhabricatorCursorPagedPolicyAwareQuery', - 'PhortunePaymentProviderTestCase' => 'PhabricatorTestCase', - 'PhortunePaypalPaymentProvider' => 'PhortunePaymentProvider', + 'PhortunePaymentProviderConfig' => array( + 'PhortuneDAO', + 'PhabricatorPolicyInterface', + ), + 'PhortunePaymentProviderConfigEditor' => 'PhabricatorApplicationTransactionEditor', + 'PhortunePaymentProviderConfigQuery' => 'PhabricatorCursorPagedPolicyAwareQuery', + 'PhortunePaymentProviderConfigTransaction' => 'PhabricatorApplicationTransaction', + 'PhortunePaymentProviderConfigTransactionQuery' => 'PhabricatorApplicationTransactionQuery', + 'PhortunePaymentProviderPHIDType' => 'PhabricatorPHIDType', 'PhortuneProduct' => array( 'PhortuneDAO', 'PhabricatorPolicyInterface', @@ -5658,7 +5669,8 @@ phutil_register_library_map(array( 'PhortuneProductPHIDType' => 'PhabricatorPHIDType', 'PhortuneProductQuery' => 'PhabricatorCursorPagedPolicyAwareQuery', 'PhortuneProductViewController' => 'PhortuneController', - 'PhortuneProviderController' => 'PhortuneController', + 'PhortuneProviderActionController' => 'PhortuneController', + 'PhortuneProviderEditController' => 'PhortuneMerchantController', 'PhortunePurchase' => array( 'PhortuneDAO', 'PhabricatorPolicyInterface', @@ -5668,7 +5680,6 @@ phutil_register_library_map(array( 'PhortunePurchaseViewController' => 'PhortuneController', 'PhortuneSchemaSpec' => 'PhabricatorConfigSchemaSpec', 'PhortuneStripePaymentProvider' => 'PhortunePaymentProvider', - 'PhortuneTestExtraPaymentProvider' => 'PhortunePaymentProvider', 'PhortuneTestPaymentProvider' => 'PhortunePaymentProvider', 'PhortuneWePayPaymentProvider' => 'PhortunePaymentProvider', 'PhragmentBrowseController' => 'PhragmentController', diff --git a/src/applications/phortune/application/PhabricatorPhortuneApplication.php b/src/applications/phortune/application/PhabricatorPhortuneApplication.php index 1aaa015e2b..7445dcb318 100644 --- a/src/applications/phortune/application/PhabricatorPhortuneApplication.php +++ b/src/applications/phortune/application/PhabricatorPhortuneApplication.php @@ -58,8 +58,11 @@ final class PhabricatorPhortuneApplication extends PhabricatorApplication { 'view/(?P\d+)/' => 'PhortuneProductViewController', 'edit/(?:(?P\d+)/)?' => 'PhortuneProductEditController', ), - 'provider/(?P[^/]+)/(?P[^/]+)/' - => 'PhortuneProviderController', + 'provider/' => array( + 'edit/(?:(?P\d+)/)?' => 'PhortuneProviderEditController', + '(?P\d+)/(?P[^/]+)/' + => 'PhortuneProviderActionController', + ), 'merchant/' => array( '(?:query/(?P[^/]+)/)?' => 'PhortuneMerchantListController', 'edit/(?:(?P\d+)/)?' => 'PhortuneMerchantEditController', diff --git a/src/applications/phortune/controller/PhortuneMerchantViewController.php b/src/applications/phortune/controller/PhortuneMerchantViewController.php index 7211de4139..9db57fcc4f 100644 --- a/src/applications/phortune/controller/PhortuneMerchantViewController.php +++ b/src/applications/phortune/controller/PhortuneMerchantViewController.php @@ -22,7 +22,7 @@ final class PhortuneMerchantViewController } $crumbs = $this->buildApplicationCrumbs(); - $crumbs->addTextCrumb(pht('Merchant %d', $merchant->getID())); + $crumbs->addTextCrumb($merchant->getName()); $title = pht( 'Merchant %d %s', @@ -39,6 +39,8 @@ final class PhortuneMerchantViewController $actions = $this->buildActionListView($merchant); $properties->setActionList($actions); + $providers = $this->buildProviderList($merchant); + $box = id(new PHUIObjectBoxView()) ->setHeader($header) ->appendChild($properties); @@ -57,6 +59,7 @@ final class PhortuneMerchantViewController array( $crumbs, $box, + $providers, $timeline, ), array( @@ -98,4 +101,57 @@ final class PhortuneMerchantViewController return $view; } + private function buildProviderList(PhortuneMerchant $merchant) { + $viewer = $this->getRequest()->getUser(); + $id = $merchant->getID(); + + $can_edit = PhabricatorPolicyFilter::hasCapability( + $viewer, + $merchant, + PhabricatorPolicyCapability::CAN_EDIT); + + $provider_list = id(new PHUIObjectItemListView()) + ->setNoDataString(pht('This merchant has no payment providers.')); + + $providers = id(new PhortunePaymentProviderConfigQuery()) + ->setViewer($viewer) + ->withMerchantPHIDs(array($merchant->getPHID())) + ->execute(); + foreach ($providers as $provider_config) { + $provider = $provider_config->buildProvider(); + $provider_id = $provider_config->getID(); + + $item = id(new PHUIObjectItemView()) + ->setObjectName(pht('Provider %d', $provider_id)) + ->setHeader($provider->getName()); + + $item->addAction( + id(new PHUIListItemView()) + ->setIcon('fa-pencil') + ->setHref($this->getApplicationURI("/provider/edit/{$provider_id}")) + ->setWorkflow(!$can_edit) + ->setDisabled(!$can_edit)); + + $provider_list->addItem($item); + } + + $add_action = id(new PHUIButtonView()) + ->setTag('a') + ->setHref($this->getApplicationURI('provider/edit/?merchantID='.$id)) + ->setText(pht('Add Payment Provider')) + ->setDisabled(!$can_edit) + ->setWorkflow(!$can_edit) + ->setIcon(id(new PHUIIconView())->setIconFont('fa-plus')); + + $header = id(new PHUIHeaderView()) + ->setHeader(pht('Payment Providers')) + ->addActionLink($add_action); + + return id(new PHUIObjectBoxView()) + ->setHeader($header) + ->appendChild($provider_list); + } + + + } diff --git a/src/applications/phortune/controller/PhortuneProviderController.php b/src/applications/phortune/controller/PhortuneProviderActionController.php similarity index 71% rename from src/applications/phortune/controller/PhortuneProviderController.php rename to src/applications/phortune/controller/PhortuneProviderActionController.php index be66ca695f..21f28709de 100644 --- a/src/applications/phortune/controller/PhortuneProviderController.php +++ b/src/applications/phortune/controller/PhortuneProviderActionController.php @@ -1,12 +1,12 @@ digest = $data['digest']; + $this->id = $data['id']; $this->setAction($data['action']); } @@ -21,24 +21,22 @@ final class PhortuneProviderController extends PhortuneController { public function processRequest() { $request = $this->getRequest(); - $user = $request->getUser(); + $viewer = $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!'); + $provider_config = id(new PhortunePaymentProviderConfigQuery()) + ->setViewer($viewer) + ->withIDs(array($this->id)) + ->executeOne(); + if (!$provider_config) { + return new Aphront404Response(); } + $provider = $provider_config->buildProvider(); + if (!$provider->canRespondToControllerAction($this->getAction())) { return new Aphront404Response(); } - $response = $provider->processControllerRequest($this, $request); if ($response instanceof AphrontResponse) { diff --git a/src/applications/phortune/controller/PhortuneProviderEditController.php b/src/applications/phortune/controller/PhortuneProviderEditController.php new file mode 100644 index 0000000000..da18fdbb45 --- /dev/null +++ b/src/applications/phortune/controller/PhortuneProviderEditController.php @@ -0,0 +1,292 @@ +id = idx($data, 'id'); + } + + public function processRequest() { + $request = $this->getRequest(); + $viewer = $request->getUser(); + + if ($this->id) { + $provider_config = id(new PhortunePaymentProviderConfigQuery()) + ->setViewer($viewer) + ->withIDs(array($this->id)) + ->requireCapabilities( + array( + PhabricatorPolicyCapability::CAN_VIEW, + PhabricatorPolicyCapability::CAN_EDIT, + )) + ->executeOne(); + if (!$provider_config) { + return new Aphront404Response(); + } + $is_new = false; + $is_choose_type = false; + + $merchant = $provider_config->getMerchant(); + $merchant_id = $merchant->getID(); + $cancel_uri = $this->getApplicationURI("merchant/{$merchant_id}/"); + } else { + $merchant = id(new PhortuneMerchantQuery()) + ->setViewer($viewer) + ->withIDs(array($request->getStr('merchantID'))) + ->requireCapabilities( + array( + PhabricatorPolicyCapability::CAN_VIEW, + PhabricatorPolicyCapability::CAN_EDIT, + )) + ->executeOne(); + if (!$merchant) { + return new Aphront404Response(); + } + $merchant_id = $merchant->getID(); + + $current_providers = id(new PhortunePaymentProviderConfigQuery()) + ->setViewer($viewer) + ->withMerchantPHIDs(array($merchant->getPHID())) + ->execute(); + $current_map = mgroup($current_providers, 'getProviderClass'); + + $provider_config = PhortunePaymentProviderConfig::initializeNewProvider( + $merchant); + + $is_new = true; + + $classes = PhortunePaymentProvider::getAllProviders(); + $class = $request->getStr('class'); + if (empty($classes[$class]) || isset($current_map[$class])) { + return $this->processChooseClassRequest( + $request, + $merchant, + $current_map); + } + + $provider_config->setProviderClass($class); + + $cancel_uri = $this->getApplicationURI( + 'provider/edit/?merchantID='.$merchant_id); + } + + $provider = $provider_config->buildProvider(); + + if ($is_new) { + $title = pht('Create Payment Provider'); + $button_text = pht('Create Provider'); + } else { + $title = pht( + 'Edit Payment Provider %d %s', + $provider_config->getID(), + $provider->getName()); + $button_text = pht('Save Changes'); + } + + $errors = array(); + if ($request->isFormPost() && $request->getStr('edit')) { + $form_values = $provider->readEditFormValuesFromRequest($request); + + list($errors, $issues, $xaction_values) = $provider->processEditForm( + $request, + $form_values); + + if (!$errors) { + // Find any secret fields which we're about to set to "*******" + // (indicating that the user did not edit the value) and remove them + // from the list of properties to update (so we don't write "******" + // to permanent configuration. + $secrets = $provider->getAllConfigurableSecretProperties(); + $secrets = array_fuse($secrets); + foreach ($xaction_values as $key => $value) { + if ($provider->isConfigurationSecret($value)) { + unset($xaction_values[$key]); + } + } + + if ($provider->canRunConfigurationTest()) { + $proxy = clone $provider; + $proxy_config = clone $provider_config; + $proxy_config->setMetadata( + $xaction_values + $provider_config->getMetadata()); + $proxy->setProviderConfig($proxy_config); + + try { + $proxy->runConfigurationTest(); + } catch (Exception $ex) { + $errors[] = pht('Unable to connect to payment provider:'); + $errors[] = $ex->getMessage(); + } + } + + if (!$errors) { + $template = id(new PhortunePaymentProviderConfigTransaction()) + ->setTransactionType( + PhortunePaymentProviderConfigTransaction::TYPE_PROPERTY); + + $xactions = array(); + + $xactions[] = id(new PhortunePaymentProviderConfigTransaction()) + ->setTransactionType( + PhortunePaymentProviderConfigTransaction::TYPE_CREATE) + ->setNewValue(true); + + foreach ($xaction_values as $key => $value) { + $xactions[] = id(clone $template) + ->setMetadataValue( + PhortunePaymentProviderConfigTransaction::PROPERTY_KEY, + $key) + ->setNewValue($value); + } + + $editor = id(new PhortunePaymentProviderConfigEditor()) + ->setActor($viewer) + ->setContentSourceFromRequest($request) + ->setContinueOnNoEffect(true); + + $editor->applyTransactions($provider_config, $xactions); + + $merchant_uri = $this->getApplicationURI( + 'merchant/'.$merchant->getID().'/'); + return id(new AphrontRedirectResponse())->setURI($merchant_uri); + } + } + } else { + $form_values = $provider->readEditFormValuesFromProviderConfig(); + $issues = array(); + } + + $form = id(new AphrontFormView()) + ->setUser($viewer) + ->addHiddenInput('merchantID', $merchant->getID()) + ->addHiddenInput('class', $provider_config->getProviderClass()) + ->addHiddenInput('edit', true) + ->appendChild( + id(new AphrontFormMarkupControl()) + ->setLabel(pht('Provider Type')) + ->setValue($provider->getName())); + + $provider->extendEditForm($request, $form, $form_values, $issues); + + $form + ->appendChild( + id(new AphrontFormSubmitControl()) + ->setValue($button_text) + ->addCancelButton($cancel_uri)) + ->appendChild( + id(new AphrontFormDividerControl())) + ->appendRemarkupInstructions( + $provider->getConfigureInstructions()); + + $crumbs = $this->buildApplicationCrumbs(); + $crumbs->addTextCrumb($merchant->getName(), $cancel_uri); + + if ($is_new) { + $crumbs->addTextCrumb(pht('Add Provider')); + } else { + $crumbs->addTextCrumb( + pht('Edit Provider %d', $provider_config->getID())); + } + + $box = id(new PHUIObjectBoxView()) + ->setFormErrors($errors) + ->setHeaderText($title) + ->appendChild($form); + + return $this->buildApplicationPage( + array( + $crumbs, + $box, + ), + array( + 'title' => $title, + )); + } + + private function processChooseClassRequest( + AphrontRequest $request, + PhortuneMerchant $merchant, + array $current_map) { + + $viewer = $request->getUser(); + + $providers = PhortunePaymentProvider::getAllProviders(); + $v_class = null; + $errors = array(); + if ($request->isFormPost()) { + $v_class = $request->getStr('class'); + if (!isset($providers[$v_class])) { + $errors[] = pht('You must select a valid provider type.'); + } + } + + $merchant_id = $merchant->getID(); + $cancel_uri = $this->getApplicationURI("merchant/{$merchant_id}/"); + + if (!$v_class) { + $v_class = key($providers); + } + + $panel_classes = id(new AphrontFormRadioButtonControl()) + ->setName('class') + ->setValue($v_class); + + $providers = msort($providers, 'getConfigureName'); + foreach ($providers as $class => $provider) { + $disabled = isset($current_map[$class]); + if ($disabled) { + $description = phutil_tag( + 'em', + array(), + pht( + 'This merchant already has a payment account configured '. + 'with this provider.')); + } else { + $description = $provider->getConfigureDescription(); + } + + $panel_classes->addButton( + $class, + $provider->getConfigureName(), + $description, + null, + $disabled); + } + + $form = id(new AphrontFormView()) + ->setUser($viewer) + ->addHiddenInput('merchantID', $merchant->getID()) + ->appendRemarkupInstructions( + pht( + 'Choose the type of payment provider to add:')) + ->appendChild($panel_classes) + ->appendChild( + id(new AphrontFormSubmitControl()) + ->setValue(pht('Continue')) + ->addCancelButton($cancel_uri)); + + $title = pht('Add Payment Provider'); + + $crumbs = $this->buildApplicationCrumbs(); + $crumbs->addTextCrumb($merchant->getName(), $cancel_uri); + $crumbs->addTextCrumb($title); + + $box = id(new PHUIObjectBoxView()) + ->setHeaderText($title) + ->setFormErrors($errors) + ->setForm($form); + + return $this->buildApplicationPage( + array( + $crumbs, + $box, + ), + array( + 'title' => $title, + )); + } + +} diff --git a/src/applications/phortune/editor/PhortunePaymentProviderConfigEditor.php b/src/applications/phortune/editor/PhortunePaymentProviderConfigEditor.php new file mode 100644 index 0000000000..ff45137230 --- /dev/null +++ b/src/applications/phortune/editor/PhortunePaymentProviderConfigEditor.php @@ -0,0 +1,81 @@ +getTransactionType()) { + case PhortunePaymentProviderConfigTransaction::TYPE_CREATE: + return null; + case PhortunePaymentProviderConfigTransaction::TYPE_PROPERTY: + $property_key = $xaction->getMetadataValue( + PhortunePaymentProviderConfigTransaction::PROPERTY_KEY); + return $object->getMetadataValue($property_key); + } + + return parent::getCustomTransactionOldValue($object, $xaction); + } + + protected function getCustomTransactionNewValue( + PhabricatorLiskDAO $object, + PhabricatorApplicationTransaction $xaction) { + + switch ($xaction->getTransactionType()) { + case PhortunePaymentProviderConfigTransaction::TYPE_CREATE: + case PhortunePaymentProviderConfigTransaction::TYPE_PROPERTY: + return $xaction->getNewValue(); + } + + return parent::getCustomTransactionNewValue($object, $xaction); + } + + protected function applyCustomInternalTransaction( + PhabricatorLiskDAO $object, + PhabricatorApplicationTransaction $xaction) { + + switch ($xaction->getTransactionType()) { + case PhortunePaymentProviderConfigTransaction::TYPE_CREATE: + return; + case PhortunePaymentProviderConfigTransaction::TYPE_PROPERTY: + $property_key = $xaction->getMetadataValue( + PhortunePaymentProviderConfigTransaction::PROPERTY_KEY); + $object->setMetadataValue($property_key, $xaction->getNewValue()); + return; + } + + return parent::applyCustomInternalTransaction($object, $xaction); + } + + protected function applyCustomExternalTransaction( + PhabricatorLiskDAO $object, + PhabricatorApplicationTransaction $xaction) { + + switch ($xaction->getTransactionType()) { + case PhortunePaymentProviderConfigTransaction::TYPE_CREATE: + case PhortunePaymentProviderConfigTransaction::TYPE_PROPERTY: + return; + } + + return parent::applyCustomExternalTransaction($object, $xaction); + } + +} diff --git a/src/applications/phortune/option/PhabricatorPhortuneConfigOptions.php b/src/applications/phortune/option/PhabricatorPhortuneConfigOptions.php deleted file mode 100644 index 6f9c4df6c4..0000000000 --- a/src/applications/phortune/option/PhabricatorPhortuneConfigOptions.php +++ /dev/null @@ -1,70 +0,0 @@ -newOption('phortune.stripe.publishable-key', 'string', null) - ->setLocked(true) - ->setDescription(pht('Stripe publishable key.')), - $this->newOption('phortune.stripe.secret-key', 'string', null) - ->setHidden(true) - ->setDescription(pht('Stripe secret key.')), - $this->newOption('phortune.balanced.marketplace-uri', 'string', null) - ->setLocked(true) - ->setDescription(pht('Balanced Marketplace URI.')), - $this->newOption('phortune.balanced.secret-key', 'string', null) - ->setHidden(true) - ->setDescription(pht('Balanced secret key.')), - $this->newOption('phortune.test.enabled', 'bool', false) - ->setBoolOptions( - array( - pht('Enable Test Provider'), - pht('Disable Test Provider'), - )) - ->setSummary(pht('Enable test payment provider.')) - ->setDescription( - pht( - "Enable the test payment provider.\n\n". - "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.')), - $this->newOption('phortune.wepay.client-id', 'string', null) - ->setLocked(true) - ->setDescription(pht('WePay application ID.')), - $this->newOption('phortune.wepay.client-secret', 'string', null) - ->setHidden(true) - ->setDescription(pht('WePay application secret.')), - $this->newOption('phortune.wepay.access-token', 'string', null) - ->setHidden(true) - ->setDescription(pht('WePay access token.')), - $this->newOption('phortune.wepay.account-id', 'string', null) - ->setLocked(true) - ->setHidden(true) - ->setDescription(pht('WePay account ID.')), - ); - } - -} diff --git a/src/applications/phortune/phid/PhortunePaymentProviderPHIDType.php b/src/applications/phortune/phid/PhortunePaymentProviderPHIDType.php new file mode 100644 index 0000000000..55be6e460c --- /dev/null +++ b/src/applications/phortune/phid/PhortunePaymentProviderPHIDType.php @@ -0,0 +1,38 @@ +withPHIDs($phids); + } + + public function loadHandles( + PhabricatorHandleQuery $query, + array $handles, + array $objects) { + + foreach ($handles as $phid => $handle) { + $provider_config = $objects[$phid]; + + $id = $provider_config->getID(); + + $handle->setName(pht('Payment Provider %d', $id)); + $handle->setURI("/phortune/provider/{$id}/"); + } + } + +} diff --git a/src/applications/phortune/provider/PhortuneBalancedPaymentProvider.php b/src/applications/phortune/provider/PhortuneBalancedPaymentProvider.php index a9204a86a1..eb9be17358 100644 --- a/src/applications/phortune/provider/PhortuneBalancedPaymentProvider.php +++ b/src/applications/phortune/provider/PhortuneBalancedPaymentProvider.php @@ -2,17 +2,118 @@ final class PhortuneBalancedPaymentProvider extends PhortunePaymentProvider { + const BALANCED_MARKETPLACE_ID = 'balanced.marketplace-id'; + const BALANCED_SECRET_KEY = 'balanced.secret-key'; + public function isEnabled() { return $this->getMarketplaceURI() && $this->getSecretKey(); } - public function getProviderType() { - return 'balanced'; + public function getName() { + return pht('Balanced Payments'); } - public function getProviderDomain() { - return 'balancedpayments.com'; + public function getConfigureName() { + return pht('Add Balanced Payments Account'); + } + + public function getConfigureDescription() { + return pht( + 'Allows you to accept credit or debit card payments with a '. + 'balancedpayments.com account.'); + } + + public function getConfigureInstructions() { + return pht( + "To configure Balacned, register or log in to an existing account on ". + "[[https://balancedpayments.com | balancedpayments.com]]. Once logged ". + "in:\n\n". + " - Choose a marketplace.\n". + " - Find the **Marketplace ID** in {nav My Marketplace > Settings} and ". + " copy it into the field above.\n". + " - On the same screen, under **API keys**, choose **Add a key**, then ". + " **Show key secret**. Copy the value into the field above.\n\n". + "You can either use a test marketplace to add this provider in test ". + "mode, or use a live marketplace to accept live payments."); + } + + public function getAllConfigurableProperties() { + return array( + self::BALANCED_MARKETPLACE_ID, + self::BALANCED_SECRET_KEY, + ); + } + + public function getAllConfigurableSecretProperties() { + return array( + self::BALANCED_SECRET_KEY, + ); + } + + public function processEditForm( + AphrontRequest $request, + array $values) { + + $errors = array(); + $issues = array(); + + if (!strlen($values[self::BALANCED_MARKETPLACE_ID])) { + $errors[] = pht('Balanced Marketplace ID is required.'); + $issues[self::BALANCED_MARKETPLACE_ID] = pht('Required'); + } + + if (!strlen($values[self::BALANCED_SECRET_KEY])) { + $errors[] = pht('Balanced Secret Key is required.'); + $issues[self::BALANCED_SECRET_KEY] = pht('Required'); + } + + return array($errors, $issues, $values); + } + + public function extendEditForm( + AphrontRequest $request, + AphrontFormView $form, + array $values, + array $issues) { + + $form + ->appendChild( + id(new AphrontFormTextControl()) + ->setName(self::BALANCED_MARKETPLACE_ID) + ->setValue($values[self::BALANCED_MARKETPLACE_ID]) + ->setError(idx($issues, self::BALANCED_MARKETPLACE_ID, true)) + ->setLabel(pht('Balanced Marketplace ID'))) + ->appendChild( + id(new AphrontFormTextControl()) + ->setName(self::BALANCED_SECRET_KEY) + ->setValue($values[self::BALANCED_SECRET_KEY]) + ->setError(idx($issues, self::BALANCED_SECRET_KEY, true)) + ->setLabel(pht('Balanced Secret Key'))); + + } + + public function canRunConfigurationTest() { + return true; + } + + public function runConfigurationTest() { + $root = dirname(phutil_get_library_root('phabricator')); + require_once $root.'/externals/httpful/bootstrap.php'; + require_once $root.'/externals/restful/bootstrap.php'; + require_once $root.'/externals/balanced-php/bootstrap.php'; + + // TODO: This only tests that the secret key is correct. It's not clear + // how to test that the marketplace is correct. + + try { + Balanced\Settings::$api_key = $this->getSecretKey(); + Balanced\APIKey::query()->first(); + } catch (RESTful\Exceptions\HTTPError $error) { + // NOTE: This exception doesn't print anything meaningful if it escapes + // to top level. Replace it with something slightly readable. + throw new Exception($error->response->body->description); + } } public function getPaymentMethodDescription() { @@ -32,11 +133,6 @@ final class PhortuneBalancedPaymentProvider extends PhortunePaymentProvider { return pht('Credit/Debit Card'); } - public function canHandlePaymentMethod(PhortunePaymentMethod $method) { - $type = $method->getMetadataValue('type'); - return ($type === 'balanced.account'); - } - protected function executeCharge( PhortunePaymentMethod $method, PhortuneCharge $charge) { @@ -79,12 +175,20 @@ final class PhortuneBalancedPaymentProvider extends PhortunePaymentProvider { $charge->save(); } - private function getMarketplaceURI() { - return PhabricatorEnv::getEnvConfig('phortune.balanced.marketplace-uri'); + private function getMarketplaceID() { + return $this + ->getProviderConfig() + ->getMetadataValue(self::BALANCED_MARKETPLACE_ID); } private function getSecretKey() { - return PhabricatorEnv::getEnvConfig('phortune.balanced.secret-key'); + return $this + ->getProviderConfig() + ->getMetadataValue(self::BALANCED_SECRET_KEY); + } + + private function getMarketplaceURI() { + return '/v1/marketplace/'.$this->getMarketplaceID(); } @@ -104,6 +208,7 @@ final class PhortuneBalancedPaymentProvider extends PhortunePaymentProvider { * @phutil-external-symbol class Balanced\Card * @phutil-external-symbol class Balanced\Settings * @phutil-external-symbol class Balanced\Marketplace + * @phutil-external-symbol class Balanced\APIKey * @phutil-external-symbol class RESTful\Exceptions\HTTPError */ public function createPaymentMethodFromRequest( diff --git a/src/applications/phortune/provider/PhortunePaypalPaymentProvider.php b/src/applications/phortune/provider/PhortunePayPalPaymentProvider.php similarity index 53% rename from src/applications/phortune/provider/PhortunePaypalPaymentProvider.php rename to src/applications/phortune/provider/PhortunePayPalPaymentProvider.php index 44af747ea9..c1632c52e1 100644 --- a/src/applications/phortune/provider/PhortunePaypalPaymentProvider.php +++ b/src/applications/phortune/provider/PhortunePayPalPaymentProvider.php @@ -1,6 +1,11 @@ getPaypalAPISignature(); } - public function getProviderType() { - return 'paypal'; + public function getName() { + return pht('PayPal'); } - public function getProviderDomain() { - return 'paypal.com'; + public function getConfigureName() { + return pht('Add PayPal Payments Account'); + } + + public function getConfigureDescription() { + return pht( + 'Allows you to accept various payment instruments with a paypal.com '. + 'account.'); + } + + public function getConfigureInstructions() { + return pht( + "To configure PayPal, register or log into an existing account on ". + "[[https://paypal.com | paypal.com]] (for live payments) or ". + "[[https://sandbox.paypal.com | sandbox.paypal.com]] (for test ". + "payments). Once logged in:\n\n". + " - Navigate to {nav Tools > API Access}.\n". + " - Choose **View API Signature**.\n". + " - Copy the **API Username**, **API Password** and **Signature** ". + " into the fields above.\n\n". + "You can select whether the provider operates in test mode or ". + "accepts live payments using the **Mode** dropdown above.\n\n". + "You can either use `sandbox.paypal.com` to retrieve live credentials, ". + "or `paypal.com` to retrieve live credentials."); + } + + public function getAllConfigurableProperties() { + return array( + self::PAYPAL_API_USERNAME, + self::PAYPAL_API_PASSWORD, + self::PAYPAL_API_SIGNATURE, + self::PAYPAL_MODE, + ); + } + + public function getAllConfigurableSecretProperties() { + return array( + self::PAYPAL_API_PASSWORD, + self::PAYPAL_API_SIGNATURE, + ); + } + + public function processEditForm( + AphrontRequest $request, + array $values) { + + $errors = array(); + $issues = array(); + + if (!strlen($values[self::PAYPAL_API_USERNAME])) { + $errors[] = pht('PayPal API Username is required.'); + $issues[self::PAYPAL_API_USERNAME] = pht('Required'); + } + + if (!strlen($values[self::PAYPAL_API_PASSWORD])) { + $errors[] = pht('PayPal API Password is required.'); + $issues[self::PAYPAL_API_PASSWORD] = pht('Required'); + } + + if (!strlen($values[self::PAYPAL_API_SIGNATURE])) { + $errors[] = pht('PayPal API Signature is required.'); + $issues[self::PAYPAL_API_SIGNATURE] = pht('Required'); + } + + if (!strlen($values[self::PAYPAL_MODE])) { + $errors[] = pht('Mode is required.'); + $issues[self::PAYPAL_MODE] = pht('Required'); + } + + return array($errors, $issues, $values); + } + + public function extendEditForm( + AphrontRequest $request, + AphrontFormView $form, + array $values, + array $issues) { + + $form + ->appendChild( + id(new AphrontFormTextControl()) + ->setName(self::PAYPAL_API_USERNAME) + ->setValue($values[self::PAYPAL_API_USERNAME]) + ->setError(idx($issues, self::PAYPAL_API_USERNAME, true)) + ->setLabel(pht('Paypal API Username'))) + ->appendChild( + id(new AphrontFormTextControl()) + ->setName(self::PAYPAL_API_PASSWORD) + ->setValue($values[self::PAYPAL_API_PASSWORD]) + ->setError(idx($issues, self::PAYPAL_API_PASSWORD, true)) + ->setLabel(pht('Paypal API Password'))) + ->appendChild( + id(new AphrontFormTextControl()) + ->setName(self::PAYPAL_API_SIGNATURE) + ->setValue($values[self::PAYPAL_API_SIGNATURE]) + ->setError(idx($issues, self::PAYPAL_API_SIGNATURE, true)) + ->setLabel(pht('Paypal API Signature'))) + ->appendChild( + id(new AphrontFormSelectControl()) + ->setName(self::PAYPAL_MODE) + ->setValue($values[self::PAYPAL_MODE]) + ->setError(idx($issues, self::PAYPAL_MODE)) + ->setLabel(pht('Mode')) + ->setOptions( + array( + 'test' => pht('Test Mode'), + 'live' => pht('Live Mode'), + ))); + + return; + } + + public function canRunConfigurationTest() { + return true; + } + + public function runConfigurationTest() { + $result = $this + ->newPaypalAPICall() + ->setRawPayPalQuery('GetBalance', array()) + ->resolve(); } public function getPaymentMethodDescription() { - return pht('Credit Card or Paypal Account'); + return pht('Credit Card or PayPal Account'); } public function getPaymentMethodIcon() { @@ -28,12 +152,7 @@ final class PhortunePaypalPaymentProvider extends PhortunePaymentProvider { } public function getPaymentMethodProviderDescription() { - return 'Paypal'; - } - - public function canHandlePaymentMethod(PhortunePaymentMethod $method) { - $type = $method->getMetadataValue('type'); - return ($type == 'paypal'); + return 'PayPal'; } protected function executeCharge( @@ -43,15 +162,21 @@ final class PhortunePaypalPaymentProvider extends PhortunePaymentProvider { } private function getPaypalAPIUsername() { - return PhabricatorEnv::getEnvConfig('phortune.paypal.api-username'); + return $this + ->getProviderConfig() + ->getMetadataValue(self::PAYPAL_API_USERNAME); } private function getPaypalAPIPassword() { - return PhabricatorEnv::getEnvConfig('phortune.paypal.api-password'); + return $this + ->getProviderConfig() + ->getMetadataValue(self::PAYPAL_API_PASSWORD); } private function getPaypalAPISignature() { - return PhabricatorEnv::getEnvConfig('phortune.paypal.api-signature'); + return $this + ->getProviderConfig() + ->getMetadataValue(self::PAYPAL_API_SIGNATURE); } /* -( One-Time Payments )-------------------------------------------------- */ @@ -74,7 +199,7 @@ final class PhortunePaypalPaymentProvider extends PhortunePaymentProvider { } public function processControllerRequest( - PhortuneProviderController $controller, + PhortuneProviderActionController $controller, AphrontRequest $request) { $viewer = $request->getUser(); @@ -214,8 +339,15 @@ final class PhortunePaypalPaymentProvider extends PhortunePaymentProvider { } private function newPaypalAPICall() { + $mode = $this->getProviderConfig()->getMetadataValue(self::PAYPAL_MODE); + if ($mode == 'live') { + $host = 'https://api-3t.paypal.com/nvp'; + } else { + $host = 'https://api-3t.sandbox.paypal.com/nvp'; + } + return id(new PhutilPayPalAPIFuture()) - ->setHost('https://api-3t.sandbox.paypal.com/nvp') + ->setHost($host) ->setAPIUsername($this->getPaypalAPIUsername()) ->setAPIPassword($this->getPaypalAPIPassword()) ->setAPISignature($this->getPaypalAPISignature()); diff --git a/src/applications/phortune/provider/PhortunePaymentProvider.php b/src/applications/phortune/provider/PhortunePaymentProvider.php index a0fa333b93..b4c9d80606 100644 --- a/src/applications/phortune/provider/PhortunePaymentProvider.php +++ b/src/applications/phortune/provider/PhortunePaymentProvider.php @@ -5,16 +5,119 @@ */ abstract class PhortunePaymentProvider { + private $providerConfig; + + public function setProviderConfig( + PhortunePaymentProviderConfig $provider_config) { + $this->providerConfig = $provider_config; + return $this; + } + + public function getProviderConfig() { + return $this->providerConfig; + } + + /** + * Return a short name which identifies this provider. + */ + abstract public function getName(); + + +/* -( Configuring Providers )---------------------------------------------- */ + + + /** + * Return a human-readable provider name for use on the merchant workflow + * where a merchant owner adds providers. + */ + abstract public function getConfigureName(); + + + /** + * Return a human-readable provider description for use on the merchant + * workflow where a merchant owner adds providers. + */ + abstract public function getConfigureDescription(); + + abstract public function getConfigureInstructions(); + + abstract public function getAllConfigurableProperties(); + + abstract public function getAllConfigurableSecretProperties(); + /** + * Read a dictionary of properties from the provider's configuration for + * use when editing the provider. + */ + public function readEditFormValuesFromProviderConfig() { + $properties = $this->getAllConfigurableProperties(); + $config = $this->getProviderConfig(); + + $secrets = $this->getAllConfigurableSecretProperties(); + $secrets = array_fuse($secrets); + + $map = array(); + foreach ($properties as $property) { + $map[$property] = $config->getMetadataValue($property); + if (isset($secrets[$property])) { + $map[$property] = $this->renderConfigurationSecret($map[$property]); + } + } + + return $map; + } + + + /** + * Read a dictionary of properties from a request for use when editing the + * provider. + */ + public function readEditFormValuesFromRequest(AphrontRequest $request) { + $properties = $this->getAllConfigurableProperties(); + + $map = array(); + foreach ($properties as $property) { + $map[$property] = $request->getStr($property); + } + + return $map; + } + + + abstract public function processEditForm( + AphrontRequest $request, + array $values); + + abstract public function extendEditForm( + AphrontRequest $request, + AphrontFormView $form, + array $values, + array $issues); + + protected function renderConfigurationSecret($value) { + if (strlen($value)) { + return str_repeat('*', strlen($value)); + } + return ''; + } + + public function isConfigurationSecret($value) { + return preg_match('/^\*+\z/', trim($value)); + } + + abstract public function canRunConfigurationTest(); + + public function runConfigurationTest() { + throw new PhortuneNotImplementedException($this); + } + /* -( Selecting Providers )------------------------------------------------ */ public static function getAllProviders() { - $objects = id(new PhutilSymbolLoader()) + return id(new PhutilSymbolLoader()) ->setAncestorClass('PhortunePaymentProvider') ->loadObjects(); - - return mpull($objects, null, 'getProviderKey'); } public static function getEnabledProviders() { @@ -47,66 +150,16 @@ abstract class PhortunePaymentProvider { 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() { - return $this->getProviderType().'@'.$this->getProviderDomain(); - } - - - /** - * Return a short string which uniquely identifies this provider's protocol - * type, like "stripe", "paypal", or "balanced". - */ - abstract public function getProviderType(); - - - /** - * Return a short string which uniquely identifies the domain for this - * provider, like "stripe.com" or "google.com". - * - * This is distinct from the provider type so that protocols are not bound - * to a single domain. This is probably not relevant for payments, but this - * assumption burned us pretty hard with authentication and it's easy enough - * to avoid. - */ - abstract public function getProviderDomain(); - abstract public function getPaymentMethodDescription(); abstract public function getPaymentMethodIcon(); abstract public function getPaymentMethodProviderDescription(); - - /** - * Determine of a provider can handle a payment method. - * - * @return bool True if this provider can apply charges to the payment method. - */ - abstract public function canHandlePaymentMethod( - PhortunePaymentMethod $method); - final public function applyCharge( PhortunePaymentMethod $payment_method, PhortuneCharge $charge) { - - $charge->setStatus(PhortuneCharge::STATUS_CHARGING); - $charge->save(); - $this->executeCharge($payment_method, $charge); - - $charge->setStatus(PhortuneCharge::STATUS_CHARGED); - $charge->save(); } abstract protected function executeCharge( @@ -230,10 +283,9 @@ abstract class PhortunePaymentProvider { array $params = array(), $local = false) { - $digest = PhabricatorHash::digestForIndex($this->getProviderKey()); - + $id = $this->getProviderConfig()->getID(); $app = PhabricatorApplication::getByClass('PhabricatorPhortuneApplication'); - $path = $app->getBaseURI().'provider/'.$digest.'/'.$action.'/'; + $path = $app->getBaseURI().'provider/'.$id.'/'.$action.'/'; $uri = new PhutilURI($path); $uri->setQueryParams($params); @@ -250,7 +302,7 @@ abstract class PhortunePaymentProvider { } public function processControllerRequest( - PhortuneProviderController $controller, + PhortuneProviderActionController $controller, AphrontRequest $request) { throw new PhortuneNotImplementedException($this); } diff --git a/src/applications/phortune/provider/PhortuneStripePaymentProvider.php b/src/applications/phortune/provider/PhortuneStripePaymentProvider.php index 957eeb9cb8..e50f6a6a57 100644 --- a/src/applications/phortune/provider/PhortuneStripePaymentProvider.php +++ b/src/applications/phortune/provider/PhortuneStripePaymentProvider.php @@ -2,17 +2,26 @@ final class PhortuneStripePaymentProvider extends PhortunePaymentProvider { + const STRIPE_PUBLISHABLE_KEY = 'stripe.publishable-key'; + const STRIPE_SECRET_KEY = 'stripe.secret-key'; + public function isEnabled() { return $this->getPublishableKey() && $this->getSecretKey(); } - public function getProviderType() { - return 'stripe'; + public function getName() { + return pht('Stripe'); } - public function getProviderDomain() { - return 'stripe.com'; + public function getConfigureName() { + return pht('Add Stripe Payments Account'); + } + + public function getConfigureDescription() { + return pht( + 'Allows you to accept credit or debit card payments with a '. + 'stripe.com account.'); } public function getPaymentMethodDescription() { @@ -32,14 +41,88 @@ final class PhortuneStripePaymentProvider extends PhortunePaymentProvider { return pht('Credit/Debit Card'); } - public function canHandlePaymentMethod(PhortunePaymentMethod $method) { - $type = $method->getMetadataValue('type'); - return ($type === 'stripe.customer'); + public function getAllConfigurableProperties() { + return array( + self::STRIPE_PUBLISHABLE_KEY, + self::STRIPE_SECRET_KEY, + ); + } + + public function getAllConfigurableSecretProperties() { + return array( + self::STRIPE_SECRET_KEY, + ); + } + + public function processEditForm( + AphrontRequest $request, + array $values) { + + $errors = array(); + $issues = array(); + + if (!strlen($values[self::STRIPE_SECRET_KEY])) { + $errors[] = pht('Stripe Secret Key is required.'); + $issues[self::STRIPE_SECRET_KEY] = pht('Required'); + } + + if (!strlen($values[self::STRIPE_PUBLISHABLE_KEY])) { + $errors[] = pht('Stripe Publishable Key is required.'); + $issues[self::STRIPE_PUBLISHABLE_KEY] = pht('Required'); + } + + return array($errors, $issues, $values); + } + + public function extendEditForm( + AphrontRequest $request, + AphrontFormView $form, + array $values, + array $issues) { + + $form + ->appendChild( + id(new AphrontFormTextControl()) + ->setName(self::STRIPE_SECRET_KEY) + ->setValue($values[self::STRIPE_SECRET_KEY]) + ->setError(idx($issues, self::STRIPE_SECRET_KEY, true)) + ->setLabel(pht('Stripe Secret Key'))) + ->appendChild( + id(new AphrontFormTextControl()) + ->setName(self::STRIPE_PUBLISHABLE_KEY) + ->setValue($values[self::STRIPE_PUBLISHABLE_KEY]) + ->setError(idx($issues, self::STRIPE_PUBLISHABLE_KEY, true)) + ->setLabel(pht('Stripe Publishable Key'))); + } + + public function getConfigureInstructions() { + return pht( + "To configure Stripe, register or log in to an existing account on ". + "[[https://stripe.com | stripe.com]]. Once logged in:\n\n". + " - Go to {nav icon=user, name=Your Account > Account Settings ". + "> API Keys}\n". + " - Copy the **Secret Key** and **Publishable Key** into the fields ". + "above.\n\n". + "You can either use the test keys to add this provider in test mode, ". + "or the live keys to accept live payments."); + } + + public function canRunConfigurationTest() { + return true; + } + + public function runConfigurationTest() { + $root = dirname(phutil_get_library_root('phabricator')); + require_once $root.'/externals/stripe-php/lib/Stripe.php'; + + $secret_key = $this->getSecretKey(); + $account = Stripe_Account::retrieve($secret_key); } /** * @phutil-external-symbol class Stripe_Charge * @phutil-external-symbol class Stripe_CardError + * @phutil-external-symbol class Stripe_Account */ protected function executeCharge( PhortunePaymentMethod $method, @@ -76,11 +159,15 @@ final class PhortuneStripePaymentProvider extends PhortunePaymentProvider { } private function getPublishableKey() { - return PhabricatorEnv::getEnvConfig('phortune.stripe.publishable-key'); + return $this + ->getProviderConfig() + ->getMetadataValue(self::STRIPE_PUBLISHABLE_KEY); } private function getSecretKey() { - return PhabricatorEnv::getEnvConfig('phortune.stripe.secret-key'); + return $this + ->getProviderConfig() + ->getMetadataValue(self::STRIPE_SECRET_KEY); } diff --git a/src/applications/phortune/provider/PhortuneTestPaymentProvider.php b/src/applications/phortune/provider/PhortuneTestPaymentProvider.php index fc181c408d..cbff927fb7 100644 --- a/src/applications/phortune/provider/PhortuneTestPaymentProvider.php +++ b/src/applications/phortune/provider/PhortuneTestPaymentProvider.php @@ -6,12 +6,27 @@ final class PhortuneTestPaymentProvider extends PhortunePaymentProvider { return PhabricatorEnv::getEnvConfig('phortune.test.enabled'); } - public function getProviderType() { - return 'test'; + public function getName() { + return pht('Test Payments'); } - public function getProviderDomain() { - return 'example.com'; + public function getConfigureName() { + return pht('Test Payments'); + } + + public function getConfigureDescription() { + return pht( + 'Adds a test provider to allow you to test payments. This allows '. + 'users to make purchases by clicking a button without actually paying '. + 'any money.'); + } + + public function getConfigureInstructions() { + return pht('This providers does not require any special configuration.'); + } + + public function canRunConfigurationTest() { + return false; } public function getPaymentMethodDescription() { @@ -31,17 +46,40 @@ final class PhortuneTestPaymentProvider extends PhortunePaymentProvider { return pht('Vast Wealth'); } - public function canHandlePaymentMethod(PhortunePaymentMethod $method) { - $type = $method->getMetadataValue('type'); - return ($type === 'test.wealth' || $type == 'test.multiple'); - } - protected function executeCharge( PhortunePaymentMethod $payment_method, PhortuneCharge $charge) { return; } + public function getAllConfigurableProperties() { + return array(); + } + + public function getAllConfigurableSecretProperties() { + return array(); + } + + public function processEditForm( + AphrontRequest $request, + array $values) { + + $errors = array(); + $issues = array(); + $values = array(); + + return array($errors, $issues, $values); + } + + public function extendEditForm( + AphrontRequest $request, + AphrontFormView $form, + array $values, + array $issues) { + return; + } + + /* -( Adding Payment Methods )--------------------------------------------- */ diff --git a/src/applications/phortune/provider/PhortuneWePayPaymentProvider.php b/src/applications/phortune/provider/PhortuneWePayPaymentProvider.php index 0e9726616c..7dacd7ed32 100644 --- a/src/applications/phortune/provider/PhortuneWePayPaymentProvider.php +++ b/src/applications/phortune/provider/PhortuneWePayPaymentProvider.php @@ -2,6 +2,11 @@ final class PhortuneWePayPaymentProvider extends PhortunePaymentProvider { + const WEPAY_CLIENT_ID = 'wepay.client-id'; + const WEPAY_CLIENT_SECRET = 'wepay.client-secret'; + const WEPAY_ACCESS_TOKEN = 'wepay.access-token'; + const WEPAY_ACCOUNT_ID = 'wepay.account-id'; + public function isEnabled() { return $this->getWePayClientID() && $this->getWePayClientSecret() && @@ -9,12 +14,133 @@ final class PhortuneWePayPaymentProvider extends PhortunePaymentProvider { $this->getWePayAccountID(); } - public function getProviderType() { - return 'wepay'; + public function getName() { + return pht('WePay'); } - public function getProviderDomain() { - return 'wepay.com'; + public function getConfigureName() { + return pht('Add WePay Payments Account'); + } + + public function getConfigureDescription() { + return pht( + 'Allows you to accept credit or debit card payments with a '. + 'wepay.com account.'); + } + + public function getConfigureInstructions() { + return pht( + "To configure WePay, register or log in to an existing account on ". + "[[https://wepay.com | wepay.com]] (for live payments) or ". + "[[https://stage.wepay.com | stage.wepay.com]] (for testing). ". + "Once logged in:\n\n". + " - Create an API application if you don't already have one.\n". + " - Click the API application name to go to the detail page.\n". + " - Copy **Client ID**, **Client Secret**, **Access Token** and ". + " **AccountID** from that page to the fields above.\n\n". + "You can either use `stage.wepay.com` to retrieve test credentials, ". + "or `wepay.com` to retrieve live credentials for accepting live ". + "payments."); + } + + public function canRunConfigurationTest() { + return true; + } + + public function runConfigurationTest() { + $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()); + $params = array( + 'client_id' => $this->getWePayClientID(), + 'client_secret' => $this->getWePayClientSecret(), + ); + + $wepay->request('app', $params); + } + + public function getAllConfigurableProperties() { + return array( + self::WEPAY_CLIENT_ID, + self::WEPAY_CLIENT_SECRET, + self::WEPAY_ACCESS_TOKEN, + self::WEPAY_ACCOUNT_ID, + ); + } + + public function getAllConfigurableSecretProperties() { + return array( + self::WEPAY_CLIENT_SECRET, + ); + } + + public function processEditForm( + AphrontRequest $request, + array $values) { + + $errors = array(); + $issues = array(); + + if (!strlen($values[self::WEPAY_CLIENT_ID])) { + $errors[] = pht('WePay Client ID is required.'); + $issues[self::WEPAY_CLIENT_ID] = pht('Required'); + } + + if (!strlen($values[self::WEPAY_CLIENT_SECRET])) { + $errors[] = pht('WePay Client Secret is required.'); + $issues[self::WEPAY_CLIENT_SECRET] = pht('Required'); + } + + if (!strlen($values[self::WEPAY_ACCESS_TOKEN])) { + $errors[] = pht('WePay Access Token is required.'); + $issues[self::WEPAY_ACCESS_TOKEN] = pht('Required'); + } + + if (!strlen($values[self::WEPAY_ACCOUNT_ID])) { + $errors[] = pht('WePay Account ID is required.'); + $issues[self::WEPAY_ACCOUNT_ID] = pht('Required'); + } + + return array($errors, $issues, $values); + } + + public function extendEditForm( + AphrontRequest $request, + AphrontFormView $form, + array $values, + array $issues) { + + $form + ->appendChild( + id(new AphrontFormTextControl()) + ->setName(self::WEPAY_CLIENT_ID) + ->setValue($values[self::WEPAY_CLIENT_ID]) + ->setError(idx($issues, self::WEPAY_CLIENT_ID, true)) + ->setLabel(pht('WePay Client ID'))) + ->appendChild( + id(new AphrontFormTextControl()) + ->setName(self::WEPAY_CLIENT_SECRET) + ->setValue($values[self::WEPAY_CLIENT_SECRET]) + ->setError(idx($issues, self::WEPAY_CLIENT_SECRET, true)) + ->setLabel(pht('WePay Client Secret'))) + ->appendChild( + id(new AphrontFormTextControl()) + ->setName(self::WEPAY_ACCESS_TOKEN) + ->setValue($values[self::WEPAY_ACCESS_TOKEN]) + ->setError(idx($issues, self::WEPAY_ACCESS_TOKEN, true)) + ->setLabel(pht('WePay Access Token'))) + ->appendChild( + id(new AphrontFormTextControl()) + ->setName(self::WEPAY_ACCOUNT_ID) + ->setValue($values[self::WEPAY_ACCOUNT_ID]) + ->setError(idx($issues, self::WEPAY_ACCOUNT_ID, true)) + ->setLabel(pht('WePay Account ID'))); + } public function getPaymentMethodDescription() { @@ -29,11 +155,6 @@ final class PhortuneWePayPaymentProvider extends PhortunePaymentProvider { return 'WePay'; } - public function canHandlePaymentMethod(PhortunePaymentMethod $method) { - $type = $method->getMetadataValue('type'); - return ($type == 'wepay'); - } - protected function executeCharge( PhortunePaymentMethod $payment_method, PhortuneCharge $charge) { @@ -41,19 +162,27 @@ final class PhortuneWePayPaymentProvider extends PhortunePaymentProvider { } private function getWePayClientID() { - return PhabricatorEnv::getEnvConfig('phortune.wepay.client-id'); + return $this + ->getProviderConfig() + ->getMetadataValue(self::WEPAY_CLIENT_ID); } private function getWePayClientSecret() { - return PhabricatorEnv::getEnvConfig('phortune.wepay.client-secret'); + return $this + ->getProviderConfig() + ->getMetadataValue(self::WEPAY_CLIENT_SECRET); } private function getWePayAccessToken() { - return PhabricatorEnv::getEnvConfig('phortune.wepay.access-token'); + return $this + ->getProviderConfig() + ->getMetadataValue(self::WEPAY_ACCESS_TOKEN); } private function getWePayAccountID() { - return PhabricatorEnv::getEnvConfig('phortune.wepay.account-id'); + return $this + ->getProviderConfig() + ->getMetadataValue(self::WEPAY_ACCOUNT_ID); } @@ -81,7 +210,7 @@ final class PhortuneWePayPaymentProvider extends PhortunePaymentProvider { * @phutil-external-symbol class WePay */ public function processControllerRequest( - PhortuneProviderController $controller, + PhortuneProviderActionController $controller, AphrontRequest $request) { $viewer = $request->getUser(); diff --git a/src/applications/phortune/provider/__tests__/PhortunePaymentProviderTestCase.php b/src/applications/phortune/provider/__tests__/PhortunePaymentProviderTestCase.php deleted file mode 100644 index cc5324d114..0000000000 --- a/src/applications/phortune/provider/__tests__/PhortunePaymentProviderTestCase.php +++ /dev/null @@ -1,51 +0,0 @@ - true, - ); - } - - public function testNoPaymentProvider() { - $env = PhabricatorEnv::beginScopedEnv(); - $env->overrideEnvConfig('phortune.test.enabled', true); - - $method = id(new PhortunePaymentMethod()) - ->setMetadataValue('type', 'hugs'); - - $caught = null; - try { - $provider = $method->buildPaymentProvider(); - } catch (Exception $ex) { - $caught = $ex; - } - - $this->assertTrue( - ($caught instanceof PhortuneNoPaymentProviderException), - 'No provider should accept hugs; they are not a currency.'); - } - - public function testMultiplePaymentProviders() { - $env = PhabricatorEnv::beginScopedEnv(); - $env->overrideEnvConfig('phortune.test.enabled', true); - - $method = id(new PhortunePaymentMethod()) - ->setMetadataValue('type', 'test.multiple'); - - $caught = null; - try { - $provider = $method->buildPaymentProvider(); - } catch (Exception $ex) { - $caught = $ex; - } - - $this->assertTrue( - ($caught instanceof PhortuneMultiplePaymentProvidersException), - 'Expect exception when more than one provider handles a payment method.'); - } - - - -} diff --git a/src/applications/phortune/provider/__tests__/PhortuneTestExtraPaymentProvider.php b/src/applications/phortune/provider/__tests__/PhortuneTestExtraPaymentProvider.php deleted file mode 100644 index 5ea6376750..0000000000 --- a/src/applications/phortune/provider/__tests__/PhortuneTestExtraPaymentProvider.php +++ /dev/null @@ -1,40 +0,0 @@ -getMetadataValue('type'); - return ($type === 'test.multiple'); - } - - protected function executeCharge( - PhortunePaymentMethod $payment_method, - PhortuneCharge $charge) { - return; - } - -} diff --git a/src/applications/phortune/query/PhortunePaymentProviderConfigQuery.php b/src/applications/phortune/query/PhortunePaymentProviderConfigQuery.php new file mode 100644 index 0000000000..748ff7cb64 --- /dev/null +++ b/src/applications/phortune/query/PhortunePaymentProviderConfigQuery.php @@ -0,0 +1,95 @@ +ids = $ids; + return $this; + } + + public function withPHIDs(array $phids) { + $this->phids = $phids; + return $this; + } + + public function withMerchantPHIDs(array $phids) { + $this->merchantPHIDs = $phids; + return $this; + } + + protected function loadPage() { + $table = new PhortunePaymentProviderConfig(); + $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 $provider_configs) { + $merchant_phids = mpull($provider_configs, 'getMerchantPHID'); + $merchants = id(new PhortuneMerchantQuery()) + ->setViewer($this->getViewer()) + ->setParentQuery($this) + ->withPHIDs($merchant_phids) + ->execute(); + $merchants = mpull($merchants, null, 'getPHID'); + + foreach ($provider_configs as $key => $config) { + $merchant = idx($merchants, $config->getMerchantPHID()); + if (!$merchant) { + $this->didRejectResult($config); + unset($provider_configs[$key]); + continue; + } + $config->attachMerchant($merchant); + } + + return $provider_configs; + } + + private function buildWhereClause(AphrontDatabaseConnection $conn) { + $where = array(); + + if ($this->ids !== null) { + $where[] = qsprintf( + $conn, + 'id IN (%Ld)', + $this->ids); + } + + if ($this->phids !== null) { + $where[] = qsprintf( + $conn, + 'phid IN (%Ls)', + $this->phids); + } + + if ($this->merchantPHIDs !== null) { + $where[] = qsprintf( + $conn, + 'merchantPHID IN (%Ls)', + $this->merchantPHIDs); + } + + $where[] = $this->buildPagingClause($conn); + + return $this->formatWhereClause($where); + } + + public function getQueryApplicationClass() { + return 'PhabricatorPhortuneApplication'; + } + +} diff --git a/src/applications/phortune/query/PhortunePaymentProviderConfigTransactionQuery.php b/src/applications/phortune/query/PhortunePaymentProviderConfigTransactionQuery.php new file mode 100644 index 0000000000..34797ba657 --- /dev/null +++ b/src/applications/phortune/query/PhortunePaymentProviderConfigTransactionQuery.php @@ -0,0 +1,10 @@ + 'text64', 'brand' => 'text64', 'expires' => 'text16', - 'providerType' => 'text16', - 'providerDomain' => 'text64', 'lastFourDigits' => 'text16', ), self::CONFIG_KEY_SCHEMA => array( @@ -75,27 +72,9 @@ final class PhortunePaymentMethod extends PhortuneDAO } public function buildPaymentProvider() { - $providers = PhortunePaymentProvider::getAllProviders(); - - $accept = array(); - foreach ($providers as $provider) { - if ($provider->canHandlePaymentMethod($this)) { - $accept[] = $provider; - } - } - - if (!$accept) { - throw new PhortuneNoPaymentProviderException($this); - } - - if (count($accept) > 1) { - throw new PhortuneMultiplePaymentProvidersException($this, $accept); - } - - return head($accept); + throw new Exception(pht('TODO: Reimplement this junk.')); } - public function getDisplayName() { if (strlen($this->name)) { return $this->name; diff --git a/src/applications/phortune/storage/PhortunePaymentProviderConfig.php b/src/applications/phortune/storage/PhortunePaymentProviderConfig.php new file mode 100644 index 0000000000..d8d7f4875c --- /dev/null +++ b/src/applications/phortune/storage/PhortunePaymentProviderConfig.php @@ -0,0 +1,96 @@ +setMerchantPHID($merchant->getPHID()); + } + + public function getConfiguration() { + return array( + self::CONFIG_AUX_PHID => true, + self::CONFIG_SERIALIZATION => array( + 'metadata' => self::SERIALIZATION_JSON, + ), + self::CONFIG_COLUMN_SCHEMA => array( + 'providerClassKey' => 'bytes12', + 'providerClass' => 'text128', + ), + self::CONFIG_KEY_SCHEMA => array( + 'key_merchant' => array( + 'columns' => array('merchantPHID', 'providerClassKey'), + 'unique' => true, + ), + ), + ) + parent::getConfiguration(); + } + + public function save() { + $this->providerClassKey = PhabricatorHash::digestForIndex( + $this->providerClass); + + return parent::save(); + } + + public function generatePHID() { + return PhabricatorPHID::generateNewPHID( + PhortunePaymentProviderPHIDType::TYPECONST); + } + + public function attachMerchant(PhortuneMerchant $merchant) { + $this->merchant = $merchant; + return $this; + } + + public function getMerchant() { + return $this->assertAttached($this->merchant); + } + + public function getMetadataValue($key, $default = null) { + return idx($this->metadata, $key, $default); + } + + public function setMetadataValue($key, $value) { + $this->metadata[$key] = $value; + return $this; + } + + public function buildProvider() { + return newv($this->getProviderClass(), array()) + ->setProviderConfig($this); + } + + +/* -( PhabricatorPolicyInterface )----------------------------------------- */ + + + public function getCapabilities() { + return array( + PhabricatorPolicyCapability::CAN_VIEW, + PhabricatorPolicyCapability::CAN_EDIT, + ); + } + + public function getPolicy($capability) { + return $this->getMerchant()->getPolicy($capability); + } + + public function hasAutomaticCapability($capability, PhabricatorUser $viewer) { + return $this->getMerchant()->hasAutomaticCapability($capability, $viewer); + } + + public function describeAutomaticCapability($capability) { + return pht('Providers have the policies of their merchant.'); + } + +} diff --git a/src/applications/phortune/storage/PhortunePaymentProviderConfigTransaction.php b/src/applications/phortune/storage/PhortunePaymentProviderConfigTransaction.php new file mode 100644 index 0000000000..3c05633bd4 --- /dev/null +++ b/src/applications/phortune/storage/PhortunePaymentProviderConfigTransaction.php @@ -0,0 +1,46 @@ +getAuthorPHID(); + + $old = $this->getOldValue(); + $new = $this->getNewValue(); + + switch ($this->getTransactionType()) { + case self::TYPE_CREATE: + return pht( + '%s created this payment provider.', + $this->renderHandleLink($author_phid)); + case self::TYPE_PROPERTY: + // TODO: Allow providers to improve this. + + return pht( + '%s edited a property of this payment provider.', + $this->renderHandleLink($author_phid)); + break; + } + + return parent::getTitle(); + } + +}