1
0
Fork 0
mirror of https://we.phorge.it/source/phorge.git synced 2024-12-18 19:40:55 +01:00

Make payment providers a configurable property of Merchants in Phortune

Summary:
Ref T2787. Instead of making providers global configuration, make them a thing on merchants with web configuration.

Payment methods and some of the pyament workflow needs to be retooled a bit after this, but this seemed like a reasonable cutoff point for this diff.

Test Plan: See screenshots.

Reviewers: btrahan

Reviewed By: btrahan

Subscribers: epriestley

Maniphest Tasks: T2787

Differential Revision: https://secure.phabricator.com/D10649
This commit is contained in:
epriestley 2014-10-07 14:41:41 -07:00
parent 3cf9a5820f
commit 9aa5a8cb7b
23 changed files with 1447 additions and 329 deletions

View file

@ -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;

View file

@ -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;

View file

@ -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',

View file

@ -58,8 +58,11 @@ final class PhabricatorPhortuneApplication extends PhabricatorApplication {
'view/(?P<id>\d+)/' => 'PhortuneProductViewController',
'edit/(?:(?P<id>\d+)/)?' => 'PhortuneProductEditController',
),
'provider/(?P<digest>[^/]+)/(?P<action>[^/]+)/'
=> 'PhortuneProviderController',
'provider/' => array(
'edit/(?:(?P<id>\d+)/)?' => 'PhortuneProviderEditController',
'(?P<id>\d+)/(?P<action>[^/]+)/'
=> 'PhortuneProviderActionController',
),
'merchant/' => array(
'(?:query/(?P<queryKey>[^/]+)/)?' => 'PhortuneMerchantListController',
'edit/(?:(?P<id>\d+)/)?' => 'PhortuneMerchantEditController',

View file

@ -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);
}
}

View file

@ -1,12 +1,12 @@
<?php
final class PhortuneProviderController extends PhortuneController {
final class PhortuneProviderActionController extends PhortuneController {
private $digest;
private $id;
private $action;
public function willProcessRequest(array $data) {
$this->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) {

View file

@ -0,0 +1,292 @@
<?php
final class PhortuneProviderEditController
extends PhortuneMerchantController {
private $id;
public function willProcessRequest(array $data) {
$this->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,
));
}
}

View file

@ -0,0 +1,81 @@
<?php
final class PhortunePaymentProviderConfigEditor
extends PhabricatorApplicationTransactionEditor {
public function getEditorApplicationClass() {
return 'PhabricatorPhortuneApplication';
}
public function getEditorObjectsDescription() {
return pht('Phortune Payment Providers');
}
public function getTransactionTypes() {
$types = parent::getTransactionTypes();
$types[] = PhortunePaymentProviderConfigTransaction::TYPE_CREATE;
$types[] = PhortunePaymentProviderConfigTransaction::TYPE_PROPERTY;
return $types;
}
protected function getCustomTransactionOldValue(
PhabricatorLiskDAO $object,
PhabricatorApplicationTransaction $xaction) {
switch ($xaction->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);
}
}

View file

@ -1,70 +0,0 @@
<?php
final class PhabricatorPhortuneConfigOptions
extends PhabricatorApplicationConfigOptions {
public function getName() {
return pht('Phortune');
}
public function getDescription() {
return pht('Configure payments and billing.');
}
public function getOptions() {
return array(
$this->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.')),
);
}
}

View file

@ -0,0 +1,38 @@
<?php
final class PhortunePaymentProviderPHIDType extends PhabricatorPHIDType {
const TYPECONST = 'PHPR';
public function getTypeName() {
return pht('Phortune Payment Provider');
}
public function newObject() {
return new PhortunePaymentProviderConfig();
}
protected function buildQueryForObjects(
PhabricatorObjectQuery $query,
array $phids) {
return id(new PhortunePaymentProviderConfigQuery())
->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}/");
}
}
}

View file

@ -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(

View file

@ -1,6 +1,11 @@
<?php
final class PhortunePaypalPaymentProvider extends PhortunePaymentProvider {
final class PhortunePayPalPaymentProvider extends PhortunePaymentProvider {
const PAYPAL_API_USERNAME = 'paypal.api-username';
const PAYPAL_API_PASSWORD = 'paypal.api-password';
const PAYPAL_API_SIGNATURE = 'paypal.api-signature';
const PAYPAL_MODE = 'paypal.mode';
public function isEnabled() {
// TODO: See note in processControllerRequest().
@ -11,16 +16,135 @@ final class PhortunePaypalPaymentProvider extends PhortunePaymentProvider {
$this->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());

View file

@ -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);
}

View file

@ -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);
}

View file

@ -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 )--------------------------------------------- */

View file

@ -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();

View file

@ -1,51 +0,0 @@
<?php
final class PhortunePaymentProviderTestCase extends PhabricatorTestCase {
public function getPhabricatorTestCaseConfiguration() {
return array(
self::PHABRICATOR_TESTCONFIG_BUILD_STORAGE_FIXTURES => 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.');
}
}

View file

@ -1,40 +0,0 @@
<?php
final class PhortuneTestExtraPaymentProvider extends PhortunePaymentProvider {
public function isEnabled() {
return false;
}
public function getProviderType() {
return 'test2';
}
public function getProviderDomain() {
return 'example.com';
}
public function getPaymentMethodDescription() {
return pht('You Should Not Be Able to See This');
}
public function getPaymentMethodIcon() {
return celerity_get_resource_uri('/rsrc/image/phortune/test.png');
}
public function getPaymentMethodProviderDescription() {
return pht('Just for Unit Tests');
}
public function canHandlePaymentMethod(PhortunePaymentMethod $method) {
$type = $method->getMetadataValue('type');
return ($type === 'test.multiple');
}
protected function executeCharge(
PhortunePaymentMethod $payment_method,
PhortuneCharge $charge) {
return;
}
}

View file

@ -0,0 +1,95 @@
<?php
final class PhortunePaymentProviderConfigQuery
extends PhabricatorCursorPagedPolicyAwareQuery {
private $ids;
private $phids;
private $merchantPHIDs;
public function withIDs(array $ids) {
$this->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';
}
}

View file

@ -0,0 +1,10 @@
<?php
final class PhortunePaymentProviderConfigTransactionQuery
extends PhabricatorApplicationTransactionQuery {
public function getTemplateApplicationTransaction() {
return new PhortunePaymentProviderConfigTransaction();
}
}

View file

@ -14,12 +14,11 @@ final class PhortunePaymentMethod extends PhortuneDAO
protected $status;
protected $accountPHID;
protected $authorPHID;
protected $providerPHID;
protected $expires;
protected $metadata = array();
protected $brand;
protected $lastFourDigits;
protected $providerType;
protected $providerDomain;
private $account = self::ATTACHABLE;
@ -34,8 +33,6 @@ final class PhortunePaymentMethod extends PhortuneDAO
'status' => '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;

View file

@ -0,0 +1,96 @@
<?php
final class PhortunePaymentProviderConfig extends PhortuneDAO
implements PhabricatorPolicyInterface {
protected $merchantPHID;
protected $providerClassKey;
protected $providerClass;
protected $metadata = array();
private $merchant = self::ATTACHABLE;
public static function initializeNewProvider(
PhortuneMerchant $merchant) {
return id(new PhortunePaymentProviderConfig())
->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.');
}
}

View file

@ -0,0 +1,46 @@
<?php
final class PhortunePaymentProviderConfigTransaction
extends PhabricatorApplicationTransaction {
const TYPE_CREATE = 'paymentprovider:create';
const TYPE_PROPERTY = 'paymentprovider:property';
const PROPERTY_KEY = 'provider-property';
public function getApplicationName() {
return 'phortune';
}
public function getApplicationTransactionType() {
return PhortunePaymentProviderPHIDType::TYPECONST;
}
public function getApplicationTransactionCommentObject() {
return null;
}
public function getTitle() {
$author_phid = $this->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();
}
}