Page MenuHomestyx hydra

No OneTemporary

diff --git a/src/applications/fund/phortune/FundBackerProduct.php b/src/applications/fund/phortune/FundBackerProduct.php
index ebe9f31843..28f3d8c7cc 100644
--- a/src/applications/fund/phortune/FundBackerProduct.php
+++ b/src/applications/fund/phortune/FundBackerProduct.php
@@ -1,125 +1,127 @@
<?php
final class FundBackerProduct extends PhortuneProductImplementation {
private $initiativePHID;
private $initiative;
private $viewer;
public function setViewer(PhabricatorUser $viewer) {
$this->viewer = $viewer;
return $this;
}
public function getViewer() {
return $this->viewer;
}
public function getRef() {
return $this->getInitiativePHID();
}
public function getName(PhortuneProduct $product) {
$initiative = $this->getInitiative();
return pht(
'Fund %s %s',
$initiative->getMonogram(),
$initiative->getName());
}
public function getPriceAsCurrency(PhortuneProduct $product) {
return PhortuneCurrency::newEmptyCurrency();
}
public function setInitiativePHID($initiative_phid) {
$this->initiativePHID = $initiative_phid;
return $this;
}
public function getInitiativePHID() {
return $this->initiativePHID;
}
public function setInitiative(FundInitiative $initiative) {
$this->initiative = $initiative;
return $this;
}
public function getInitiative() {
return $this->initiative;
}
public function loadImplementationsForRefs(
PhabricatorUser $viewer,
array $refs) {
$initiatives = id(new FundInitiativeQuery())
->setViewer($viewer)
->withPHIDs($refs)
->execute();
$initiatives = mpull($initiatives, null, 'getPHID');
$objects = array();
foreach ($refs as $ref) {
$object = id(new FundBackerProduct())
->setViewer($viewer)
->setInitiativePHID($ref);
$initiative = idx($initiatives, $ref);
if ($initiative) {
$object->setInitiative($initiative);
}
$objects[] = $object;
}
return $objects;
}
public function didPurchaseProduct(
PhortuneProduct $product,
PhortunePurchase $purchase) {
+ // TODO: This viewer may be wrong if the purchase completes after a hold
+ // we should load the backer explicitly.
$viewer = $this->getViewer();
$backer = id(new FundBackerQuery())
->setViewer($viewer)
->withPHIDs(array($purchase->getMetadataValue('backerPHID')))
->executeOne();
if (!$backer) {
throw new Exception(pht('Unable to load FundBacker!'));
}
$xactions = array();
$xactions[] = id(new FundBackerTransaction())
->setTransactionType(FundBackerTransaction::TYPE_STATUS)
->setNewValue(FundBacker::STATUS_PURCHASED);
$editor = id(new FundBackerEditor())
->setActor($viewer)
->setContentSource($this->getContentSource());
$editor->applyTransactions($backer, $xactions);
$xactions = array();
$xactions[] = id(new FundInitiativeTransaction())
->setTransactionType(FundInitiativeTransaction::TYPE_BACKER)
->setNewValue($backer->getPHID());
$editor = id(new FundInitiativeEditor())
->setActor($viewer)
->setContentSource($this->getContentSource());
$editor->applyTransactions($this->getInitiative(), $xactions);
return;
}
public function didRefundProduct(
PhortuneProduct $product,
PhortunePurchase $purchase) {
$viewer = $this->getViewer();
// TODO: Undonate.
}
}
diff --git a/src/applications/phortune/controller/PhortuneAccountViewController.php b/src/applications/phortune/controller/PhortuneAccountViewController.php
index 89d6a84149..40da7157ce 100644
--- a/src/applications/phortune/controller/PhortuneAccountViewController.php
+++ b/src/applications/phortune/controller/PhortuneAccountViewController.php
@@ -1,285 +1,294 @@
<?php
final class PhortuneAccountViewController extends PhortuneController {
private $accountID;
public function willProcessRequest(array $data) {
$this->accountID = $data['accountID'];
}
public function processRequest() {
$request = $this->getRequest();
$user = $request->getUser();
// TODO: Currently, you must be able to edit an account to view the detail
// page, because the account must be broadly visible so merchants can
// process orders but merchants should not be able to see all the details
// of an account. Ideally this page should be visible to merchants, too,
// just with less information.
$account = id(new PhortuneAccountQuery())
->setViewer($user)
->withIDs(array($this->accountID))
->requireCapabilities(
array(
PhabricatorPolicyCapability::CAN_VIEW,
PhabricatorPolicyCapability::CAN_EDIT,
))
->executeOne();
if (!$account) {
return new Aphront404Response();
}
$title = $account->getName();
$crumbs = $this->buildApplicationCrumbs();
$crumbs->addTextCrumb(pht('Account'), $request->getRequestURI());
$header = id(new PHUIHeaderView())
->setHeader($title);
$actions = id(new PhabricatorActionListView())
->setUser($user)
->setObjectURI($request->getRequestURI())
->addAction(
id(new PhabricatorActionView())
->setName(pht('Edit Account'))
->setIcon('fa-pencil')
->setHref('#')
->setDisabled(true))
->addAction(
id(new PhabricatorActionView())
->setName(pht('Edit Members'))
->setIcon('fa-users')
->setHref('#')
->setDisabled(true));
$crumbs->setActionList($actions);
$properties = id(new PHUIPropertyListView())
->setObject($account)
->setUser($user);
$properties->setActionList($actions);
$payment_methods = $this->buildPaymentMethodsSection($account);
$purchase_history = $this->buildPurchaseHistorySection($account);
$charge_history = $this->buildChargeHistorySection($account);
$account_history = $this->buildAccountHistorySection($account);
$object_box = id(new PHUIObjectBoxView())
->setHeader($header)
->addPropertyList($properties);
return $this->buildApplicationPage(
array(
$crumbs,
$object_box,
$payment_methods,
$purchase_history,
$charge_history,
$account_history,
),
array(
'title' => $title,
));
}
private function buildPaymentMethodsSection(PhortuneAccount $account) {
$request = $this->getRequest();
$viewer = $request->getUser();
$can_edit = PhabricatorPolicyFilter::hasCapability(
$viewer,
$account,
PhabricatorPolicyCapability::CAN_EDIT);
$id = $account->getID();
$header = id(new PHUIHeaderView())
->setHeader(pht('Payment Methods'));
$list = id(new PHUIObjectItemListView())
->setUser($viewer)
->setNoDataString(
pht('No payment methods associated with this account.'));
$methods = id(new PhortunePaymentMethodQuery())
->setViewer($viewer)
->withAccountPHIDs(array($account->getPHID()))
->execute();
if ($methods) {
$this->loadHandles(mpull($methods, 'getAuthorPHID'));
}
foreach ($methods as $method) {
$id = $method->getID();
$item = new PHUIObjectItemView();
$item->setHeader($method->getFullDisplayName());
switch ($method->getStatus()) {
case PhortunePaymentMethod::STATUS_ACTIVE:
$item->setBarColor('green');
$disable_uri = $this->getApplicationURI('card/'.$id.'/disable/');
$item->addAction(
id(new PHUIListItemView())
->setIcon('fa-times')
->setHref($disable_uri)
->setDisabled(!$can_edit)
->setWorkflow(true));
break;
case PhortunePaymentMethod::STATUS_DISABLED:
$item->setDisabled(true);
break;
}
$provider = $method->buildPaymentProvider();
$item->addAttribute($provider->getPaymentMethodProviderDescription());
$edit_uri = $this->getApplicationURI('card/'.$id.'/edit/');
$item->addAction(
id(new PHUIListItemView())
->setIcon('fa-pencil')
->setHref($edit_uri)
->setDisabled(!$can_edit)
->setWorkflow(!$can_edit));
$list->addItem($item);
}
return id(new PHUIObjectBoxView())
->setHeader($header)
->appendChild($list);
}
private function buildPurchaseHistorySection(PhortuneAccount $account) {
$request = $this->getRequest();
$viewer = $request->getUser();
$carts = id(new PhortuneCartQuery())
->setViewer($viewer)
->withAccountPHIDs(array($account->getPHID()))
->needPurchases(true)
->withStatuses(
array(
PhortuneCart::STATUS_PURCHASING,
+ PhortuneCart::STATUS_CHARGED,
+ PhortuneCart::STATUS_HOLD,
PhortuneCart::STATUS_PURCHASED,
))
->execute();
$phids = array();
foreach ($carts as $cart) {
$phids[] = $cart->getPHID();
foreach ($cart->getPurchases() as $purchase) {
$phids[] = $purchase->getPHID();
}
}
$handles = $this->loadViewerHandles($phids);
$rows = array();
$rowc = array();
foreach ($carts as $cart) {
$cart_link = $handles[$cart->getPHID()]->renderLink();
$purchases = $cart->getPurchases();
if (count($purchases) == 1) {
$purchase_name = $handles[$purchase->getPHID()]->renderLink();
$purchases = array();
} else {
$purchase_name = '';
}
$rowc[] = '';
$rows[] = array(
+ $cart->getID(),
phutil_tag(
'strong',
array(),
$cart_link),
$purchase_name,
phutil_tag(
'strong',
array(),
$cart->getTotalPriceAsCurrency()->formatForDisplay()),
+ PhortuneCart::getNameForStatus($cart->getStatus()),
phabricator_datetime($cart->getDateModified(), $viewer),
);
foreach ($purchases as $purchase) {
$id = $purchase->getID();
$price = $purchase->getTotalPriceAsCurrency()->formatForDisplay();
$rowc[] = '';
$rows[] = array(
'',
$handles[$purchase->getPHID()]->renderLink(),
$price,
'',
+ '',
);
}
}
$table = id(new AphrontTableView($rows))
->setRowClasses($rowc)
->setHeaders(
array(
- pht('Cart'),
+ pht('ID'),
+ pht('Order'),
pht('Purchase'),
pht('Amount'),
+ pht('Status'),
pht('Updated'),
))
->setColumnClasses(
array(
+ '',
'',
'wide',
'right',
+ '',
'right',
));
$header = id(new PHUIHeaderView())
- ->setHeader(pht('Purchase History'));
+ ->setHeader(pht('Order History'));
return id(new PHUIObjectBoxView())
->setHeader($header)
->appendChild($table);
}
private function buildChargeHistorySection(PhortuneAccount $account) {
$request = $this->getRequest();
$viewer = $request->getUser();
$charges = id(new PhortuneChargeQuery())
->setViewer($viewer)
->withAccountPHIDs(array($account->getPHID()))
->needCarts(true)
->execute();
return $this->buildChargesTable($charges);
}
private function buildAccountHistorySection(PhortuneAccount $account) {
$request = $this->getRequest();
$user = $request->getUser();
$xactions = id(new PhortuneAccountTransactionQuery())
->setViewer($user)
->withObjectPHIDs(array($account->getPHID()))
->execute();
$engine = id(new PhabricatorMarkupEngine())
->setViewer($user);
$xaction_view = id(new PhabricatorApplicationTransactionView())
->setUser($user)
->setObjectPHID($account->getPHID())
->setTransactions($xactions)
->setMarkupEngine($engine);
return $xaction_view;
}
}
diff --git a/src/applications/phortune/controller/PhortuneCartUpdateController.php b/src/applications/phortune/controller/PhortuneCartUpdateController.php
index 8bbd6eaa95..b9477fc68b 100644
--- a/src/applications/phortune/controller/PhortuneCartUpdateController.php
+++ b/src/applications/phortune/controller/PhortuneCartUpdateController.php
@@ -1,31 +1,65 @@
<?php
final class PhortuneCartUpdateController
extends PhortuneCartController {
private $id;
public function willProcessRequest(array $data) {
$this->id = $data['id'];
}
public function processRequest() {
$request = $this->getRequest();
$viewer = $request->getUser();
$cart = id(new PhortuneCartQuery())
->setViewer($viewer)
->withIDs(array($this->id))
->needPurchases(true)
->executeOne();
if (!$cart) {
return new Aphront404Response();
}
- // TODO: This obviously doesn't do anything for now.
+ $charges = id(new PhortuneChargeQuery())
+ ->setViewer($viewer)
+ ->withCartPHIDs(array($cart->getPHID()))
+ ->needCarts(true)
+ ->withStatuses(
+ array(
+ PhortuneCharge::STATUS_HOLD,
+ PhortuneCharge::STATUS_CHARGED,
+ ))
+ ->execute();
+
+ if ($charges) {
+ $providers = id(new PhortunePaymentProviderConfigQuery())
+ ->setViewer($viewer)
+ ->withPHIDs(mpull($charges, 'getProviderPHID'))
+ ->execute();
+ $providers = mpull($providers, null, 'getPHID');
+ } else {
+ $providers = array();
+ }
+
+ foreach ($charges as $charge) {
+ if ($charge->isRefund()) {
+ // Don't update refunds.
+ continue;
+ }
+
+ $provider_config = idx($providers, $charge->getProviderPHID());
+ if (!$provider_config) {
+ throw new Exception(pht('Unable to load provider for charge!'));
+ }
+
+ $provider = $provider_config->buildProvider();
+ $provider->updateCharge($charge);
+ }
return id(new AphrontRedirectResponse())
->setURI($cart->getDetailURI());
}
}
diff --git a/src/applications/phortune/controller/PhortuneCartViewController.php b/src/applications/phortune/controller/PhortuneCartViewController.php
index 8f2b54c248..b909a996f5 100644
--- a/src/applications/phortune/controller/PhortuneCartViewController.php
+++ b/src/applications/phortune/controller/PhortuneCartViewController.php
@@ -1,211 +1,210 @@
<?php
final class PhortuneCartViewController
extends PhortuneCartController {
private $id;
public function willProcessRequest(array $data) {
$this->id = $data['id'];
}
public function processRequest() {
$request = $this->getRequest();
$viewer = $request->getUser();
$cart = id(new PhortuneCartQuery())
->setViewer($viewer)
->withIDs(array($this->id))
->needPurchases(true)
->executeOne();
if (!$cart) {
return new Aphront404Response();
}
$can_admin = PhabricatorPolicyFilter::hasCapability(
$viewer,
$cart->getMerchant(),
PhabricatorPolicyCapability::CAN_EDIT);
$cart_table = $this->buildCartContentTable($cart);
$can_edit = PhabricatorPolicyFilter::hasCapability(
$viewer,
$cart,
PhabricatorPolicyCapability::CAN_EDIT);
$errors = array();
$resume_uri = null;
switch ($cart->getStatus()) {
case PhortuneCart::STATUS_PURCHASING:
if ($can_edit) {
$resume_uri = $cart->getMetadataValue('provider.checkoutURI');
if ($resume_uri) {
$errors[] = pht(
'The checkout process has been started, but not yet completed. '.
'You can continue checking out by clicking %s, or cancel the '.
'order, or contact the merchant for assistance.',
phutil_tag('strong', array(), pht('Continue Checkout')));
} else {
$errors[] = pht(
'The checkout process has been started, but an error occurred. '.
'You can cancel the order or contact the merchant for '.
'assistance.');
}
}
break;
case PhortuneCart::STATUS_CHARGED:
if ($can_edit) {
$errors[] = pht(
'You have been charged, but processing could not be completed. '.
'You can cancel your order, or contact the merchant for '.
'assistance.');
}
break;
case PhortuneCart::STATUS_HOLD:
if ($can_edit) {
$errors[] = pht(
'Payment for this order is on hold. You can click %s to check '.
'for updates, cancel the order, or contact the merchant for '.
'assistance.',
phutil_tag('strong', array(), pht('Update Status')));
}
break;
}
$properties = $this->buildPropertyListView($cart);
$actions = $this->buildActionListView(
$cart,
$can_edit,
$can_admin,
$resume_uri);
$properties->setActionList($actions);
$header = id(new PHUIHeaderView())
->setUser($viewer)
- ->setHeader(pht('Order Detail'))
- ->setPolicyObject($cart);
+ ->setHeader(pht('Order Detail'));
$cart_box = id(new PHUIObjectBoxView())
->setHeader($header)
->setFormErrors($errors)
->appendChild($properties)
->appendChild($cart_table);
$charges = id(new PhortuneChargeQuery())
->setViewer($viewer)
->withCartPHIDs(array($cart->getPHID()))
->needCarts(true)
->execute();
$charges_table = $this->buildChargesTable($charges, false);
$account = $cart->getAccount();
$crumbs = $this->buildApplicationCrumbs();
$this->addAccountCrumb($crumbs, $cart->getAccount());
$crumbs->addTextCrumb(pht('Cart %d', $cart->getID()));
return $this->buildApplicationPage(
array(
$crumbs,
$cart_box,
$charges_table,
),
array(
'title' => pht('Cart'),
));
}
private function buildPropertyListView(PhortuneCart $cart) {
$viewer = $this->getRequest()->getUser();
$view = id(new PHUIPropertyListView())
->setUser($viewer)
->setObject($cart);
$handles = $this->loadViewerHandles(
array(
$cart->getAccountPHID(),
$cart->getAuthorPHID(),
$cart->getMerchantPHID(),
));
$view->addProperty(
pht('Order Name'),
$cart->getName());
$view->addProperty(
pht('Account'),
$handles[$cart->getAccountPHID()]->renderLink());
$view->addProperty(
pht('Authorized By'),
$handles[$cart->getAuthorPHID()]->renderLink());
$view->addProperty(
pht('Merchant'),
$handles[$cart->getMerchantPHID()]->renderLink());
$view->addProperty(
pht('Status'),
PhortuneCart::getNameForStatus($cart->getStatus()));
$view->addProperty(
pht('Updated'),
phabricator_datetime($cart->getDateModified(), $viewer));
return $view;
}
private function buildActionListView(
PhortuneCart $cart,
$can_edit,
$can_admin,
$resume_uri) {
$viewer = $this->getRequest()->getUser();
$id = $cart->getID();
$view = id(new PhabricatorActionListView())
->setUser($viewer)
->setObject($cart);
$can_cancel = ($can_edit && $cart->canCancelOrder());
$cancel_uri = $this->getApplicationURI("cart/{$id}/cancel/");
$refund_uri = $this->getApplicationURI("cart/{$id}/refund/");
$update_uri = $this->getApplicationURI("cart/{$id}/update/");
$view->addAction(
id(new PhabricatorActionView())
->setName(pht('Cancel Order'))
->setIcon('fa-times')
->setDisabled(!$can_cancel)
->setWorkflow(true)
->setHref($cancel_uri));
if ($can_admin) {
$view->addAction(
id(new PhabricatorActionView())
->setName(pht('Refund Order'))
->setIcon('fa-reply')
->setWorkflow(true)
->setHref($refund_uri));
}
$view->addAction(
id(new PhabricatorActionView())
->setName(pht('Update Status'))
->setIcon('fa-refresh')
->setHref($update_uri));
if ($can_edit && $resume_uri) {
$view->addAction(
id(new PhabricatorActionView())
->setName(pht('Continue Checkout'))
->setIcon('fa-shopping-cart')
->setHref($resume_uri));
}
return $view;
}
}
diff --git a/src/applications/phortune/provider/PhortuneBalancedPaymentProvider.php b/src/applications/phortune/provider/PhortuneBalancedPaymentProvider.php
index 92d1d64fb0..87ab157716 100644
--- a/src/applications/phortune/provider/PhortuneBalancedPaymentProvider.php
+++ b/src/applications/phortune/provider/PhortuneBalancedPaymentProvider.php
@@ -1,360 +1,370 @@
<?php
final class PhortuneBalancedPaymentProvider extends PhortunePaymentProvider {
const BALANCED_MARKETPLACE_ID = 'balanced.marketplace-id';
const BALANCED_SECRET_KEY = 'balanced.secret-key';
public function isAcceptingLivePayments() {
return !preg_match('/-test-/', $this->getSecretKey());
}
public function getName() {
return pht('Balanced Payments');
}
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 getConfigureProvidesDescription() {
return pht(
'This merchant accepts credit and debit cards via Balanced Payments.');
}
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';
+ $this->loadBalancedAPILibraries();
// 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() {
return pht('Add Credit or Debit Card');
}
public function getPaymentMethodIcon() {
return 'Balanced';
}
public function getPaymentMethodProviderDescription() {
return pht('Processed by Balanced');
}
public function getDefaultPaymentMethodDisplayName(
PhortunePaymentMethod $method) {
return pht('Credit/Debit Card');
}
protected function executeCharge(
PhortunePaymentMethod $method,
PhortuneCharge $charge) {
-
- $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';
+ $this->loadBalancedAPILibraries();
$price = $charge->getAmountAsCurrency();
// Build the string which will appear on the credit card statement.
$charge_as = new PhutilURI(PhabricatorEnv::getProductionURI('/'));
$charge_as = $charge_as->getDomain();
$charge_as = id(new PhutilUTF8StringTruncator())
->setMaximumBytes(22)
->setTerminator('')
->truncateString($charge_as);
try {
Balanced\Settings::$api_key = $this->getSecretKey();
$card = Balanced\Card::get($method->getMetadataValue('balanced.cardURI'));
$debit = $card->debit($price->getValueInUSDCents(), $charge_as);
} 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);
}
$expect_status = 'succeeded';
if ($debit->status !== $expect_status) {
throw new Exception(
pht(
'Debit failed, expected "%s", got "%s".',
$expect_status,
$debit->status));
}
$charge->setMetadataValue('balanced.debitURI', $debit->uri);
$charge->save();
}
protected function executeRefund(
PhortuneCharge $charge,
PhortuneCharge $refund) {
-
- $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';
+ $this->loadBalancedAPILibraries();
$debit_uri = $charge->getMetadataValue('balanced.debitURI');
if (!$debit_uri) {
throw new Exception(pht('No Balanced debit URI!'));
}
$refund_cents = $refund
->getAmountAsCurrency()
->negate()
->getValueInUSDCents();
$params = array(
'amount' => $refund_cents,
);
try {
Balanced\Settings::$api_key = $this->getSecretKey();
$balanced_debit = Balanced\Debit::get($debit_uri);
$balanced_refund = $balanced_debit->refunds->create($params);
} catch (RESTful\Exceptions\HTTPError $error) {
throw new Exception($error->response->body->description);
}
$refund->setMetadataValue('balanced.refundURI', $balanced_refund->uri);
$refund->save();
}
+ public function updateCharge(PhortuneCharge $charge) {
+ $this->loadBalancedAPILibraries();
+
+ $debit_uri = $charge->getMetadataValue('balanced.debitURI');
+ if (!$debit_uri) {
+ throw new Exception(pht('No Balanced debit URI!'));
+ }
+
+ try {
+ Balanced\Settings::$api_key = $this->getSecretKey();
+ $balanced_debit = Balanced\Debit::get($debit_uri);
+ } catch (RESTful\Exceptions\HTTPError $error) {
+ throw new Exception($error->response->body->description);
+ }
+
+ // TODO: Deal with disputes / chargebacks / surprising refunds.
+ }
+
private function getMarketplaceID() {
return $this
->getProviderConfig()
->getMetadataValue(self::BALANCED_MARKETPLACE_ID);
}
private function getSecretKey() {
return $this
->getProviderConfig()
->getMetadataValue(self::BALANCED_SECRET_KEY);
}
private function getMarketplaceURI() {
return '/v1/marketplaces/'.$this->getMarketplaceID();
}
/* -( Adding Payment Methods )--------------------------------------------- */
public function canCreatePaymentMethods() {
return true;
}
public function validateCreatePaymentMethodToken(array $token) {
return isset($token['balancedMarketplaceURI']);
}
/**
* @phutil-external-symbol class Balanced\Card
* @phutil-external-symbol class Balanced\Debit
* @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(
AphrontRequest $request,
PhortunePaymentMethod $method,
array $token) {
+ $this->loadBalancedAPILibraries();
$errors = array();
- $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';
-
$account_phid = $method->getAccountPHID();
$author_phid = $method->getAuthorPHID();
$description = $account_phid.':'.$author_phid;
try {
Balanced\Settings::$api_key = $this->getSecretKey();
$card = Balanced\Card::get($token['balancedMarketplaceURI']);
$buyer = Balanced\Marketplace::mine()->createBuyer(
null,
$card->uri,
array(
'description' => $description,
));
} 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);
}
$method
->setBrand($card->brand)
->setLastFourDigits($card->last_four)
->setExpires($card->expiration_year, $card->expiration_month)
->setMetadata(
array(
'type' => 'balanced.account',
'balanced.accountURI' => $buyer->uri,
'balanced.cardURI' => $card->uri,
));
return $errors;
}
public function renderCreatePaymentMethodForm(
AphrontRequest $request,
array $errors) {
$ccform = id(new PhortuneCreditCardForm())
->setUser($request->getUser())
->setErrors($errors)
->addScript('https://js.balancedpayments.com/v1/balanced.js');
Javelin::initBehavior(
'balanced-payment-form',
array(
'balancedMarketplaceURI' => $this->getMarketplaceURI(),
'formID' => $ccform->getFormID(),
));
return $ccform->buildForm();
}
private function getBalancedShortErrorCode($error_code) {
$prefix = 'cc:balanced:';
if (strncmp($error_code, $prefix, strlen($prefix))) {
return null;
}
return substr($error_code, strlen($prefix));
}
public function translateCreatePaymentMethodErrorCode($error_code) {
$short_code = $this->getBalancedShortErrorCode($error_code);
if ($short_code) {
static $map = array(
);
if (isset($map[$short_code])) {
return $map[$short_code];
}
}
return $error_code;
}
public function getCreatePaymentMethodErrorMessage($error_code) {
$short_code = $this->getBalancedShortErrorCode($error_code);
if (!$short_code) {
return null;
}
switch ($short_code) {
default:
break;
}
return null;
}
+ private function loadBalancedAPILibraries() {
+ $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';
+ }
+
}
diff --git a/src/applications/phortune/provider/PhortunePayPalPaymentProvider.php b/src/applications/phortune/provider/PhortunePayPalPaymentProvider.php
index 300f5de4f2..258d8875f2 100644
--- a/src/applications/phortune/provider/PhortunePayPalPaymentProvider.php
+++ b/src/applications/phortune/provider/PhortunePayPalPaymentProvider.php
@@ -1,449 +1,506 @@
<?php
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 isAcceptingLivePayments() {
$mode = $this->getProviderConfig()->getMetadataValue(self::PAYPAL_MODE);
return ($mode === 'live');
}
public function getName() {
return pht('PayPal');
}
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 getConfigureProvidesDescription() {
return pht(
'This merchant accepts payments via PayPal.');
}
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');
}
public function getPaymentMethodIcon() {
return 'PayPal';
}
public function getPaymentMethodProviderDescription() {
return 'PayPal';
}
protected function executeCharge(
PhortunePaymentMethod $payment_method,
PhortuneCharge $charge) {
throw new Exception('!');
}
protected function executeRefund(
PhortuneCharge $charge,
PhortuneCharge $refund) {
$transaction_id = $charge->getMetadataValue('paypal.transactionID');
if (!$transaction_id) {
throw new Exception(pht('Charge has no transaction ID!'));
}
$refund_amount = $refund->getAmountAsCurrency()->negate();
$refund_currency = $refund_amount->getCurrency();
$refund_value = $refund_amount->formatBareValue();
$params = array(
'TRANSACTIONID' => $transaction_id,
'REFUNDTYPE' => 'Partial',
'AMT' => $refund_value,
'CURRENCYCODE' => $refund_currency,
);
$result = $this
->newPaypalAPICall()
->setRawPayPalQuery('RefundTransaction', $params)
->resolve();
$charge->setMetadataValue(
'paypal.refundID',
$result['REFUNDTRANSACTIONID']);
}
+ public function updateCharge(PhortuneCharge $charge) {
+ $transaction_id = $charge->getMetadataValue('paypal.transactionID');
+ if (!$transaction_id) {
+ throw new Exception(pht('Charge has no transaction ID!'));
+ }
+
+ $params = array(
+ 'TRANSACTIONID' => $transaction_id,
+ );
+
+ $result = $this
+ ->newPaypalAPICall()
+ ->setRawPayPalQuery('GetTransactionDetails', $params)
+ ->resolve();
+
+ $is_charge = false;
+ $is_fail = false;
+ switch ($result['PAYMENTSTATUS']) {
+ case 'Processed':
+ case 'Completed':
+ case 'Completed-Funds-Held':
+ $is_charge = true;
+ break;
+ case 'Partially-Refunded':
+ case 'Refunded':
+ case 'Reversed':
+ case 'Canceled-Reversal':
+ // TODO: Handle these.
+ return;
+ case 'In-Progress':
+ case 'Pending':
+ // TODO: Also handle these better?
+ return;
+ case 'Denied':
+ case 'Expired':
+ case 'Failed':
+ case 'None':
+ case 'Voided':
+ default:
+ $is_fail = true;
+ break;
+ }
+
+ if ($charge->getStatus() == PhortuneCharge::STATUS_HOLD) {
+ $cart = $charge->getCart();
+
+ $unguarded = AphrontWriteGuard::beginScopedUnguardedWrites();
+ if ($is_charge) {
+ $cart->didApplyCharge($charge);
+ } else if ($is_fail) {
+ $cart->didFailCharge($charge);
+ }
+ unset($unguarded);
+ }
+ }
+
private function getPaypalAPIUsername() {
return $this
->getProviderConfig()
->getMetadataValue(self::PAYPAL_API_USERNAME);
}
private function getPaypalAPIPassword() {
return $this
->getProviderConfig()
->getMetadataValue(self::PAYPAL_API_PASSWORD);
}
private function getPaypalAPISignature() {
return $this
->getProviderConfig()
->getMetadataValue(self::PAYPAL_API_SIGNATURE);
}
/* -( One-Time Payments )-------------------------------------------------- */
public function canProcessOneTimePayments() {
return true;
}
/* -( Controllers )-------------------------------------------------------- */
public function canRespondToControllerAction($action) {
switch ($action) {
case 'checkout':
case 'charge':
case 'cancel':
return true;
}
return parent::canRespondToControllerAction();
}
public function processControllerRequest(
PhortuneProviderActionController $controller,
AphrontRequest $request) {
$viewer = $request->getUser();
$cart = $controller->loadCart($request->getInt('cartID'));
if (!$cart) {
return new Aphront404Response();
}
$charge = $controller->loadActiveCharge($cart);
switch ($controller->getAction()) {
case 'checkout':
if ($charge) {
throw new Exception(pht('Cart is already charging!'));
}
break;
case 'charge':
case 'cancel':
if (!$charge) {
throw new Exception(pht('Cart is not charging yet!'));
}
break;
}
switch ($controller->getAction()) {
case 'checkout':
$return_uri = $this->getControllerURI(
'charge',
array(
'cartID' => $cart->getID(),
));
$cancel_uri = $this->getControllerURI(
'cancel',
array(
'cartID' => $cart->getID(),
));
$price = $cart->getTotalPriceAsCurrency();
$charge = $cart->willApplyCharge($viewer, $this);
$params = array(
'PAYMENTREQUEST_0_AMT' => $price->formatBareValue(),
'PAYMENTREQUEST_0_CURRENCYCODE' => $price->getCurrency(),
'PAYMENTREQUEST_0_PAYMENTACTION' => 'Sale',
'PAYMENTREQUEST_0_CUSTOM' => $charge->getPHID(),
+ 'PAYMENTREQUEST_0_DESC' => $cart->getName(),
'RETURNURL' => $return_uri,
'CANCELURL' => $cancel_uri,
// TODO: This should be cart-dependent if we eventually support
// physical goods.
'NOSHIPPING' => '1',
);
$result = $this
->newPaypalAPICall()
->setRawPayPalQuery('SetExpressCheckout', $params)
->resolve();
$uri = new PhutilURI('https://www.sandbox.paypal.com/cgi-bin/webscr');
$uri->setQueryParams(
array(
'cmd' => '_express-checkout',
'token' => $result['TOKEN'],
));
$cart->setMetadataValue('provider.checkoutURI', (string)$uri);
$cart->save();
$charge->setMetadataValue('paypal.token', $result['TOKEN']);
$charge->save();
return id(new AphrontRedirectResponse())
->setIsExternal(true)
->setURI($uri);
case 'charge':
if ($cart->getStatus() !== PhortuneCart::STATUS_PURCHASING) {
return id(new AphrontRedirectResponse())
->setURI($cart->getCheckoutURI());
}
$token = $request->getStr('token');
$params = array(
'TOKEN' => $token,
);
$result = $this
->newPaypalAPICall()
->setRawPayPalQuery('GetExpressCheckoutDetails', $params)
->resolve();
if ($result['CUSTOM'] !== $charge->getPHID()) {
throw new Exception(
pht('Paypal checkout does not match Phortune charge!'));
}
if ($result['CHECKOUTSTATUS'] !== 'PaymentActionNotInitiated') {
return $controller->newDialog()
->setTitle(pht('Payment Already Processed'))
->appendParagraph(
pht(
'The payment response for this charge attempt has already '.
'been processed.'))
->addCancelButton($cart->getCheckoutURI(), pht('Continue'));
}
$price = $cart->getTotalPriceAsCurrency();
$params = array(
'TOKEN' => $token,
'PAYERID' => $result['PAYERID'],
'PAYMENTREQUEST_0_AMT' => $price->formatBareValue(),
'PAYMENTREQUEST_0_CURRENCYCODE' => $price->getCurrency(),
'PAYMENTREQUEST_0_PAYMENTACTION' => 'Sale',
);
$result = $this
->newPaypalAPICall()
->setRawPayPalQuery('DoExpressCheckoutPayment', $params)
->resolve();
$transaction_id = $result['PAYMENTINFO_0_TRANSACTIONID'];
$success = false;
$hold = false;
switch ($result['PAYMENTINFO_0_PAYMENTSTATUS']) {
case 'Processed':
case 'Completed':
case 'Completed-Funds-Held':
$success = true;
break;
case 'In-Progress':
case 'Pending':
// TODO: We can capture more information about this stuff.
$hold = true;
break;
case 'Denied':
case 'Expired':
case 'Failed':
case 'Partially-Refunded':
case 'Canceled-Reversal':
case 'None':
case 'Refunded':
case 'Reversed':
case 'Voided':
default:
// These are all failure states.
break;
}
$unguarded = AphrontWriteGuard::beginScopedUnguardedWrites();
$charge->setMetadataValue('paypal.transactionID', $transaction_id);
$charge->save();
if ($success) {
$cart->didApplyCharge($charge);
$response = id(new AphrontRedirectResponse())->setURI(
$cart->getDoneURI());
} else if ($hold) {
$cart->didHoldCharge($charge);
$response = $controller
->newDialog()
->setTitle(pht('Charge On Hold'))
->appendParagraph(
pht('Your charge is on hold, for reasons?'))
->addCancelButton($cart->getCheckoutURI(), pht('Continue'));
} else {
$cart->didFailCharge($charge);
$response = $controller
->newDialog()
->setTitle(pht('Charge Failed'))
->addCancelButton($cart->getCheckoutURI(), pht('Continue'));
}
unset($unguarded);
return $response;
case 'cancel':
if ($cart->getStatus() !== PhortuneCart::STATUS_PURCHASING) {
$unguarded = AphrontWriteGuard::beginScopedUnguardedWrites();
// TODO: Since the user cancelled this, we could conceivably just
// throw it away or make it more clear that it's a user cancel.
$cart->didFailCharge($charge);
unset($unguarded);
}
return id(new AphrontRedirectResponse())
->setURI($cart->getCheckoutURI());
}
throw new Exception(
pht('Unsupported action "%s".', $controller->getAction()));
}
private function newPaypalAPICall() {
if ($this->isAcceptingLivePayments()) {
$host = 'https://api-3t.paypal.com/nvp';
} else {
$host = 'https://api-3t.sandbox.paypal.com/nvp';
}
return id(new PhutilPayPalAPIFuture())
->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 ab31dd04cc..49c80dcd72 100644
--- a/src/applications/phortune/provider/PhortunePaymentProvider.php
+++ b/src/applications/phortune/provider/PhortunePaymentProvider.php
@@ -1,294 +1,296 @@
<?php
/**
* @task addmethod Adding Payment Methods
*/
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 getConfigureProvidesDescription();
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() {
return id(new PhutilSymbolLoader())
->setAncestorClass('PhortunePaymentProvider')
->loadObjects();
}
public function isEnabled() {
return $this->getProviderConfig()->getIsEnabled();
}
abstract public function isAcceptingLivePayments();
abstract public function getPaymentMethodDescription();
abstract public function getPaymentMethodIcon();
abstract public function getPaymentMethodProviderDescription();
final public function applyCharge(
PhortunePaymentMethod $payment_method,
PhortuneCharge $charge) {
$this->executeCharge($payment_method, $charge);
}
final public function refundCharge(
PhortuneCharge $charge,
PhortuneCharge $refund) {
$this->executeRefund($charge, $refund);
}
abstract protected function executeCharge(
PhortunePaymentMethod $payment_method,
PhortuneCharge $charge);
abstract protected function executeRefund(
PhortuneCharge $charge,
- PhortuneCharge $charge);
+ PhortuneCharge $refund);
+
+ abstract public function updateCharge(PhortuneCharge $charge);
/* -( Adding Payment Methods )--------------------------------------------- */
/**
* @task addmethod
*/
public function canCreatePaymentMethods() {
return false;
}
/**
* @task addmethod
*/
public function translateCreatePaymentMethodErrorCode($error_code) {
throw new PhortuneNotImplementedException($this);
}
/**
* @task addmethod
*/
public function getCreatePaymentMethodErrorMessage($error_code) {
throw new PhortuneNotImplementedException($this);
}
/**
* @task addmethod
*/
public function validateCreatePaymentMethodToken(array $token) {
throw new PhortuneNotImplementedException($this);
}
/**
* @task addmethod
*/
public function createPaymentMethodFromRequest(
AphrontRequest $request,
PhortunePaymentMethod $method,
array $token) {
throw new PhortuneNotImplementedException($this);
}
/**
* @task addmethod
*/
public function renderCreatePaymentMethodForm(
AphrontRequest $request,
array $errors) {
throw new PhortuneNotImplementedException($this);
}
public function getDefaultPaymentMethodDisplayName(
PhortunePaymentMethod $method) {
throw new PhortuneNotImplementedException($this);
}
/* -( One-Time Payments )-------------------------------------------------- */
public function canProcessOneTimePayments() {
return false;
}
public function renderOneTimePaymentButton(
PhortuneAccount $account,
PhortuneCart $cart,
PhabricatorUser $user) {
require_celerity_resource('phortune-css');
$description = $this->getPaymentMethodProviderDescription();
$details = $this->getPaymentMethodDescription();
$icon = id(new PHUIIconView())
->setSpriteSheet(PHUIIconView::SPRITE_LOGIN)
->setSpriteIcon($this->getPaymentMethodIcon());
$button = id(new PHUIButtonView())
->setSize(PHUIButtonView::BIG)
->setColor(PHUIButtonView::GREY)
->setIcon($icon)
->setText($description)
->setSubtext($details);
// NOTE: We generate a local URI to make sure the form picks up CSRF tokens.
$uri = $this->getControllerURI(
'checkout',
array(
'cartID' => $cart->getID(),
),
$local = true);
return phabricator_form(
$user,
array(
'action' => $uri,
'method' => 'POST',
),
$button);
}
/* -( Controllers )-------------------------------------------------------- */
final public function getControllerURI(
$action,
array $params = array(),
$local = false) {
$id = $this->getProviderConfig()->getID();
$app = PhabricatorApplication::getByClass('PhabricatorPhortuneApplication');
$path = $app->getBaseURI().'provider/'.$id.'/'.$action.'/';
$uri = new PhutilURI($path);
$uri->setQueryParams($params);
if ($local) {
return $uri;
} else {
return PhabricatorEnv::getURI((string)$uri);
}
}
public function canRespondToControllerAction($action) {
return false;
}
public function processControllerRequest(
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 62b17ed906..d0ed2b0b00 100644
--- a/src/applications/phortune/provider/PhortuneStripePaymentProvider.php
+++ b/src/applications/phortune/provider/PhortuneStripePaymentProvider.php
@@ -1,365 +1,379 @@
<?php
final class PhortuneStripePaymentProvider extends PhortunePaymentProvider {
const STRIPE_PUBLISHABLE_KEY = 'stripe.publishable-key';
const STRIPE_SECRET_KEY = 'stripe.secret-key';
public function isAcceptingLivePayments() {
return preg_match('/_live_/', $this->getPublishableKey());
}
public function getName() {
return pht('Stripe');
}
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 getConfigureProvidesDescription() {
return pht(
'This merchant accepts credit and debit cards via Stripe.');
}
public function getPaymentMethodDescription() {
return pht('Add Credit or Debit Card (US and Canada)');
}
public function getPaymentMethodIcon() {
return 'Stripe';
}
public function getPaymentMethodProviderDescription() {
return pht('Processed by Stripe');
}
public function getDefaultPaymentMethodDisplayName(
PhortunePaymentMethod $method) {
return pht('Credit/Debit Card');
}
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';
+ $this->loadStripeAPILibraries();
$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,
PhortuneCharge $charge) {
-
- $root = dirname(phutil_get_library_root('phabricator'));
- require_once $root.'/externals/stripe-php/lib/Stripe.php';
+ $this->loadStripeAPILibraries();
$price = $charge->getAmountAsCurrency();
$secret_key = $this->getSecretKey();
$params = array(
'amount' => $price->getValueInUSDCents(),
'currency' => $price->getCurrency(),
'customer' => $method->getMetadataValue('stripe.customerID'),
'description' => $charge->getPHID(),
'capture' => true,
);
$stripe_charge = Stripe_Charge::create($params, $secret_key);
$id = $stripe_charge->id;
if (!$id) {
throw new Exception('Stripe charge call did not return an ID!');
}
$charge->setMetadataValue('stripe.chargeID', $id);
$charge->save();
}
protected function executeRefund(
PhortuneCharge $charge,
PhortuneCharge $refund) {
+ $this->loadStripeAPILibraries();
$charge_id = $charge->getMetadataValue('stripe.chargeID');
if (!$charge_id) {
throw new Exception(
pht('Unable to refund charge; no Stripe chargeID!'));
}
- $root = dirname(phutil_get_library_root('phabricator'));
- require_once $root.'/externals/stripe-php/lib/Stripe.php';
-
$refund_cents = $refund
->getAmountAsCurrency()
->negate()
->getValueInUSDCents();
$secret_key = $this->getSecretKey();
$params = array(
'amount' => $refund_cents,
);
$stripe_charge = Stripe_Charge::retrieve($charge_id, $secret_key);
$stripe_refund = $stripe_charge->refunds->create($params);
$id = $stripe_refund->id;
if (!$id) {
throw new Exception(pht('Stripe refund call did not return an ID!'));
}
$charge->setMetadataValue('stripe.refundID', $id);
$charge->save();
}
+ public function updateCharge(PhortuneCharge $charge) {
+ $this->loadStripeAPILibraries();
+
+ $charge_id = $charge->getMetadataValue('stripe.chargeID');
+ if (!$charge_id) {
+ throw new Exception(
+ pht('Unable to update charge; no Stripe chargeID!'));
+ }
+
+ $secret_key = $this->getSecretKey();
+ $stripe_charge = Stripe_Charge::retrieve($charge_id, $secret_key);
+
+ // TODO: Deal with disputes / chargebacks / surprising refunds.
+
+ }
+
private function getPublishableKey() {
return $this
->getProviderConfig()
->getMetadataValue(self::STRIPE_PUBLISHABLE_KEY);
}
private function getSecretKey() {
return $this
->getProviderConfig()
->getMetadataValue(self::STRIPE_SECRET_KEY);
}
/* -( Adding Payment Methods )--------------------------------------------- */
public function canCreatePaymentMethods() {
return true;
}
/**
* @phutil-external-symbol class Stripe_Token
* @phutil-external-symbol class Stripe_Customer
*/
public function createPaymentMethodFromRequest(
AphrontRequest $request,
PhortunePaymentMethod $method,
array $token) {
+ $this->loadStripeAPILibraries();
$errors = array();
- $root = dirname(phutil_get_library_root('phabricator'));
- require_once $root.'/externals/stripe-php/lib/Stripe.php';
-
$secret_key = $this->getSecretKey();
$stripe_token = $token['stripeCardToken'];
// First, make sure the token is valid.
$info = id(new Stripe_Token())->retrieve($stripe_token, $secret_key);
$account_phid = $method->getAccountPHID();
$author_phid = $method->getAuthorPHID();
$params = array(
'card' => $stripe_token,
'description' => $account_phid.':'.$author_phid,
);
// Then, we need to create a Customer in order to be able to charge
// the card more than once. We create one Customer for each card;
// they do not map to PhortuneAccounts because we allow an account to
// have more than one active card.
$customer = Stripe_Customer::create($params, $secret_key);
$card = $info->card;
$method
->setBrand($card->brand)
->setLastFourDigits($card->last4)
->setExpires($card->exp_year, $card->exp_month)
->setMetadata(
array(
'type' => 'stripe.customer',
'stripe.customerID' => $customer->id,
'stripe.cardToken' => $stripe_token,
));
return $errors;
}
public function renderCreatePaymentMethodForm(
AphrontRequest $request,
array $errors) {
$ccform = id(new PhortuneCreditCardForm())
->setUser($request->getUser())
->setErrors($errors)
->addScript('https://js.stripe.com/v2/');
Javelin::initBehavior(
'stripe-payment-form',
array(
'stripePublishableKey' => $this->getPublishableKey(),
'formID' => $ccform->getFormID(),
));
return $ccform->buildForm();
}
private function getStripeShortErrorCode($error_code) {
$prefix = 'cc:stripe:';
if (strncmp($error_code, $prefix, strlen($prefix))) {
return null;
}
return substr($error_code, strlen($prefix));
}
public function validateCreatePaymentMethodToken(array $token) {
return isset($token['stripeCardToken']);
}
public function translateCreatePaymentMethodErrorCode($error_code) {
$short_code = $this->getStripeShortErrorCode($error_code);
if ($short_code) {
static $map = array(
'error:invalid_number' => PhortuneErrCode::ERR_CC_INVALID_NUMBER,
'error:invalid_cvc' => PhortuneErrCode::ERR_CC_INVALID_CVC,
'error:invalid_expiry_month' => PhortuneErrCode::ERR_CC_INVALID_EXPIRY,
'error:invalid_expiry_year' => PhortuneErrCode::ERR_CC_INVALID_EXPIRY,
);
if (isset($map[$short_code])) {
return $map[$short_code];
}
}
return $error_code;
}
/**
* See https://stripe.com/docs/api#errors for more information on possible
* errors.
*/
public function getCreatePaymentMethodErrorMessage($error_code) {
$short_code = $this->getStripeShortErrorCode($error_code);
if (!$short_code) {
return null;
}
switch ($short_code) {
case 'error:incorrect_number':
$error_key = 'number';
$message = pht('Invalid or incorrect credit card number.');
break;
case 'error:incorrect_cvc':
$error_key = 'cvc';
$message = pht('Card CVC is invalid or incorrect.');
break;
$error_key = 'exp';
$message = pht('Card expiration date is invalid or incorrect.');
break;
case 'error:invalid_expiry_month':
case 'error:invalid_expiry_year':
case 'error:invalid_cvc':
case 'error:invalid_number':
// NOTE: These should be translated into Phortune error codes earlier,
// so we don't expect to receive them here. They are listed for clarity
// and completeness. If we encounter one, we treat it as an unknown
// error.
break;
case 'error:invalid_amount':
case 'error:missing':
case 'error:card_declined':
case 'error:expired_card':
case 'error:duplicate_transaction':
case 'error:processing_error':
default:
// NOTE: These errors currently don't recevive a detailed message.
// NOTE: We can also end up here with "http:nnn" messages.
// TODO: At least some of these should have a better message, or be
// translated into common errors above.
break;
}
return null;
}
+ private function loadStripeAPILibraries() {
+ $root = dirname(phutil_get_library_root('phabricator'));
+ require_once $root.'/externals/stripe-php/lib/Stripe.php';
+ }
+
}
diff --git a/src/applications/phortune/provider/PhortuneTestPaymentProvider.php b/src/applications/phortune/provider/PhortuneTestPaymentProvider.php
index c485e8b65d..3139d2692e 100644
--- a/src/applications/phortune/provider/PhortuneTestPaymentProvider.php
+++ b/src/applications/phortune/provider/PhortuneTestPaymentProvider.php
@@ -1,154 +1,158 @@
<?php
final class PhortuneTestPaymentProvider extends PhortunePaymentProvider {
public function isAcceptingLivePayments() {
return false;
}
public function getName() {
return pht('Test Payments');
}
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 getConfigureProvidesDescription() {
return pht('This merchant accepts test payments.');
}
public function getConfigureInstructions() {
return pht('This providers does not require any special configuration.');
}
public function canRunConfigurationTest() {
return false;
}
public function getPaymentMethodDescription() {
return pht('Add Mountain of Virtual Wealth');
}
public function getPaymentMethodIcon() {
return 'TestPayment';
}
public function getPaymentMethodProviderDescription() {
return pht('Infinite Free Money');
}
public function getDefaultPaymentMethodDisplayName(
PhortunePaymentMethod $method) {
return pht('Vast Wealth');
}
protected function executeCharge(
PhortunePaymentMethod $payment_method,
PhortuneCharge $charge) {
return;
}
protected function executeRefund(
PhortuneCharge $charge,
PhortuneCharge $refund) {
return;
}
+ public function updateCharge(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 )--------------------------------------------- */
public function canCreatePaymentMethods() {
return true;
}
public function translateCreatePaymentMethodErrorCode($error_code) {
return $error_code;
}
public function getCreatePaymentMethodErrorMessage($error_code) {
return null;
}
public function validateCreatePaymentMethodToken(array $token) {
return true;
}
public function createPaymentMethodFromRequest(
AphrontRequest $request,
PhortunePaymentMethod $method,
array $token) {
$method
->setExpires('2050', '01')
->setBrand('FreeMoney')
->setLastFourDigits('9999')
->setMetadata(
array(
'type' => 'test.wealth',
));
return array();
}
/**
* @task addmethod
*/
public function renderCreatePaymentMethodForm(
AphrontRequest $request,
array $errors) {
$ccform = id(new PhortuneCreditCardForm())
->setUser($request->getUser())
->setErrors($errors);
Javelin::initBehavior(
'test-payment-form',
array(
'formID' => $ccform->getFormID(),
));
return $ccform->buildForm();
}
}
diff --git a/src/applications/phortune/provider/PhortuneWePayPaymentProvider.php b/src/applications/phortune/provider/PhortuneWePayPaymentProvider.php
index c4d927f8da..0bc54fe193 100644
--- a/src/applications/phortune/provider/PhortuneWePayPaymentProvider.php
+++ b/src/applications/phortune/provider/PhortuneWePayPaymentProvider.php
@@ -1,392 +1,405 @@
<?php
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 isAcceptingLivePayments() {
return preg_match('/^PRODUCTION_/', $this->getWePayAccessToken());
}
public function getName() {
return pht('WePay');
}
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 getConfigureProvidesDescription() {
return pht('This merchant accepts credit and debit cards via WePay.');
}
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';
+ $this->loadWePayAPILibraries();
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() {
return pht('Credit or Debit Card');
}
public function getPaymentMethodIcon() {
return 'WePay';
}
public function getPaymentMethodProviderDescription() {
return 'WePay';
}
protected function executeCharge(
PhortunePaymentMethod $payment_method,
PhortuneCharge $charge) {
throw new Exception('!');
}
private function getWePayClientID() {
return $this
->getProviderConfig()
->getMetadataValue(self::WEPAY_CLIENT_ID);
}
private function getWePayClientSecret() {
return $this
->getProviderConfig()
->getMetadataValue(self::WEPAY_CLIENT_SECRET);
}
private function getWePayAccessToken() {
return $this
->getProviderConfig()
->getMetadataValue(self::WEPAY_ACCESS_TOKEN);
}
private function getWePayAccountID() {
return $this
->getProviderConfig()
->getMetadataValue(self::WEPAY_ACCOUNT_ID);
}
protected function executeRefund(
PhortuneCharge $charge,
PhortuneCharge $refund) {
+ $wepay = $this->loadWePayAPILibraries();
- $root = dirname(phutil_get_library_root('phabricator'));
- require_once $root.'/externals/wepay/wepay.php';
-
- WePay::useStaging(
- $this->getWePayClientID(),
- $this->getWePayClientSecret());
-
- $wepay = new WePay($this->getWePayAccessToken());
-
- $charge_id = $charge->getMetadataValue('wepay.checkoutID');
+ $checkout_id = $this->getWePayCheckoutID($charge);
$params = array(
- 'checkout_id' => $charge_id,
+ 'checkout_id' => $checkout_id,
'refund_reason' => pht('Refund'),
'amount' => $refund->getAmountAsCurrency()->negate()->formatBareValue(),
);
$wepay->request('checkout/refund', $params);
}
+ public function updateCharge(PhortuneCharge $charge) {
+ $wepay = $this->loadWePayAPILibraries();
+
+ $params = array(
+ 'checkout_id' => $this->getWePayCheckoutID($charge),
+ );
+ $wepay_checkout = $wepay->request('checkout', $params);
+
+ // TODO: Deal with disputes / chargebacks / surprising refunds.
+ }
+
+
/* -( One-Time Payments )-------------------------------------------------- */
public function canProcessOneTimePayments() {
return true;
}
/* -( Controllers )-------------------------------------------------------- */
public function canRespondToControllerAction($action) {
switch ($action) {
case 'checkout':
case 'charge':
case 'cancel':
return true;
}
return parent::canRespondToControllerAction();
}
/**
* @phutil-external-symbol class WePay
*/
public function processControllerRequest(
PhortuneProviderActionController $controller,
AphrontRequest $request) {
+ $wepay = $this->loadWePayAPILibraries();
$viewer = $request->getUser();
$cart = $controller->loadCart($request->getInt('cartID'));
if (!$cart) {
return new Aphront404Response();
}
- $root = dirname(phutil_get_library_root('phabricator'));
- require_once $root.'/externals/wepay/wepay.php';
-
- WePay::useStaging(
- $this->getWePayClientID(),
- $this->getWePayClientSecret());
-
- $wepay = new WePay($this->getWePayAccessToken());
-
$charge = $controller->loadActiveCharge($cart);
switch ($controller->getAction()) {
case 'checkout':
if ($charge) {
throw new Exception(pht('Cart is already charging!'));
}
break;
case 'charge':
case 'cancel':
if (!$charge) {
throw new Exception(pht('Cart is not charging yet!'));
}
break;
}
switch ($controller->getAction()) {
case 'checkout':
$return_uri = $this->getControllerURI(
'charge',
array(
'cartID' => $cart->getID(),
));
$cancel_uri = $this->getControllerURI(
'cancel',
array(
'cartID' => $cart->getID(),
));
$price = $cart->getTotalPriceAsCurrency();
$params = array(
'account_id' => $this->getWePayAccountID(),
'short_description' => $cart->getName(),
'type' => 'SERVICE',
'amount' => $price->formatBareValue(),
'long_description' => $cart->getName(),
'reference_id' => $cart->getPHID(),
'app_fee' => 0,
'fee_payer' => 'Payee',
'redirect_uri' => $return_uri,
'fallback_uri' => $cancel_uri,
// NOTE: If we don't `auto_capture`, we might get a result back in
// either an "authorized" or a "reserved" state. We can't capture
// an "authorized" result, so just autocapture.
'auto_capture' => true,
'require_shipping' => 0,
'shipping_fee' => 0,
'charge_tax' => 0,
'mode' => 'regular',
// TODO: We could accept bank accounts but the hold/capture rules
// are not quite clear. Just accept credit cards for now.
'funding_sources' => 'cc',
);
$charge = $cart->willApplyCharge($viewer, $this);
$result = $wepay->request('checkout/create', $params);
$cart->setMetadataValue('provider.checkoutURI', $result->checkout_uri);
$cart->save();
$charge->setMetadataValue('wepay.checkoutID', $result->checkout_id);
$charge->save();
$uri = new PhutilURI($result->checkout_uri);
return id(new AphrontRedirectResponse())
->setIsExternal(true)
->setURI($uri);
case 'charge':
if ($cart->getStatus() !== PhortuneCart::STATUS_PURCHASING) {
return id(new AphrontRedirectResponse())
->setURI($cart->getCheckoutURI());
}
$checkout_id = $request->getInt('checkout_id');
$params = array(
'checkout_id' => $checkout_id,
);
$checkout = $wepay->request('checkout', $params);
if ($checkout->reference_id != $cart->getPHID()) {
throw new Exception(
pht('Checkout reference ID does not match cart PHID!'));
}
$unguarded = AphrontWriteGuard::beginScopedUnguardedWrites();
switch ($checkout->state) {
case 'authorized':
case 'reserved':
case 'captured':
// TODO: Are these all really "done" states, and not "hold"
// states? Cards and bank accounts both come back as "authorized"
// on the staging environment. Figure out what happens in
// production?
$cart->didApplyCharge($charge);
$response = id(new AphrontRedirectResponse())->setURI(
$cart->getDoneURI());
break;
default:
// It's not clear if we can ever get here on the web workflow,
// WePay doesn't seem to return back to us after a failure (the
// workflow dead-ends instead).
$cart->didFailCharge($charge);
$response = $controller
->newDialog()
->setTitle(pht('Charge Failed'))
->appendParagraph(
pht(
'Unable to make payment (checkout state is "%s").',
$checkout->state))
->addCancelButton($cart->getCheckoutURI(), pht('Continue'));
break;
}
unset($unguarded);
return $response;
case 'cancel':
// TODO: I don't know how it's possible to cancel out of a WePay
// charge workflow.
throw new Exception(
pht('How did you get here? WePay has no cancel flow in its UI...?'));
break;
}
throw new Exception(
pht('Unsupported action "%s".', $controller->getAction()));
}
+ private function loadWePayAPILibraries() {
+ $root = dirname(phutil_get_library_root('phabricator'));
+ require_once $root.'/externals/wepay/wepay.php';
+
+ WePay::useStaging(
+ $this->getWePayClientID(),
+ $this->getWePayClientSecret());
+
+ return new WePay($this->getWePayAccessToken());
+ }
+
+ private function getWePayCheckoutID(PhortuneCharge $charge) {
+ $checkout_id = $charge->getMetadataValue('wepay.checkoutID');
+ if ($checkout_id === null) {
+ throw new Exception(pht('No WePay Checkout ID present on charge!'));
+ }
+ return $checkout_id;
+ }
}
diff --git a/src/applications/phortune/storage/PhortuneCart.php b/src/applications/phortune/storage/PhortuneCart.php
index 0f5159648d..4b0225c68c 100644
--- a/src/applications/phortune/storage/PhortuneCart.php
+++ b/src/applications/phortune/storage/PhortuneCart.php
@@ -1,527 +1,525 @@
<?php
final class PhortuneCart extends PhortuneDAO
implements PhabricatorPolicyInterface {
const STATUS_BUILDING = 'cart:building';
const STATUS_READY = 'cart:ready';
const STATUS_PURCHASING = 'cart:purchasing';
const STATUS_CHARGED = 'cart:charged';
const STATUS_HOLD = 'cart:hold';
const STATUS_PURCHASED = 'cart:purchased';
protected $accountPHID;
protected $authorPHID;
protected $merchantPHID;
protected $cartClass;
protected $status;
protected $metadata = array();
private $account = self::ATTACHABLE;
private $purchases = self::ATTACHABLE;
private $implementation = self::ATTACHABLE;
private $merchant = self::ATTACHABLE;
public static function initializeNewCart(
PhabricatorUser $actor,
PhortuneAccount $account,
PhortuneMerchant $merchant) {
$cart = id(new PhortuneCart())
->setAuthorPHID($actor->getPHID())
->setStatus(self::STATUS_BUILDING)
->setAccountPHID($account->getPHID())
->setMerchantPHID($merchant->getPHID());
$cart->account = $account;
$cart->purchases = array();
return $cart;
}
public function newPurchase(
PhabricatorUser $actor,
PhortuneProduct $product) {
$purchase = PhortunePurchase::initializeNewPurchase($actor, $product)
->setAccountPHID($this->getAccount()->getPHID())
->setCartPHID($this->getPHID())
->save();
$this->purchases[] = $purchase;
return $purchase;
}
public static function getStatusNameMap() {
return array(
self::STATUS_BUILDING => pht('Building'),
self::STATUS_READY => pht('Ready'),
self::STATUS_PURCHASING => pht('Purchasing'),
self::STATUS_CHARGED => pht('Charged'),
self::STATUS_HOLD => pht('Hold'),
self::STATUS_PURCHASED => pht('Purchased'),
);
}
public static function getNameForStatus($status) {
return idx(self::getStatusNameMap(), $status, $status);
}
public function activateCart() {
$this->setStatus(self::STATUS_READY)->save();
return $this;
}
public function willApplyCharge(
PhabricatorUser $actor,
PhortunePaymentProvider $provider,
PhortunePaymentMethod $method = null) {
$account = $this->getAccount();
$charge = PhortuneCharge::initializeNewCharge()
->setAccountPHID($account->getPHID())
->setCartPHID($this->getPHID())
->setAuthorPHID($actor->getPHID())
->setMerchantPHID($this->getMerchant()->getPHID())
->setProviderPHID($provider->getProviderConfig()->getPHID())
->setAmountAsCurrency($this->getTotalPriceAsCurrency());
if ($method) {
$charge->setPaymentMethodPHID($method->getPHID());
}
$this->openTransaction();
$this->beginReadLocking();
$copy = clone $this;
$copy->reload();
if ($copy->getStatus() !== self::STATUS_READY) {
throw new Exception(
pht(
'Cart has wrong status ("%s") to call willApplyCharge(), '.
'expected "%s".',
$copy->getStatus(),
self::STATUS_READY));
}
$charge->save();
$this->setStatus(PhortuneCart::STATUS_PURCHASING)->save();
$this->endReadLocking();
$this->saveTransaction();
return $charge;
}
public function didHoldCharge(PhortuneCharge $charge) {
$charge->setStatus(PhortuneCharge::STATUS_HOLD);
$this->openTransaction();
$this->beginReadLocking();
$copy = clone $this;
$copy->reload();
if ($copy->getStatus() !== self::STATUS_PURCHASING) {
throw new Exception(
pht(
'Cart has wrong status ("%s") to call didHoldCharge(), '.
'expected "%s".',
$copy->getStatus(),
self::STATUS_PURCHASING));
}
$charge->save();
$this->setStatus(self::STATUS_HOLD)->save();
$this->endReadLocking();
$this->saveTransaction();
}
public function didApplyCharge(PhortuneCharge $charge) {
$charge->setStatus(PhortuneCharge::STATUS_CHARGED);
$this->openTransaction();
$this->beginReadLocking();
$copy = clone $this;
$copy->reload();
- if ($copy->getStatus() !== self::STATUS_PURCHASING) {
+ if (($copy->getStatus() !== self::STATUS_PURCHASING) &&
+ ($copy->getStatus() !== self::STATUS_HOLD)) {
throw new Exception(
pht(
- 'Cart has wrong status ("%s") to call didApplyCharge(), '.
- 'expected "%s".',
- $copy->getStatus(),
- self::STATUS_PURCHASING));
+ 'Cart has wrong status ("%s") to call didApplyCharge().',
+ $copy->getStatus()));
}
$charge->save();
$this->setStatus(self::STATUS_CHARGED)->save();
$this->endReadLocking();
$this->saveTransaction();
foreach ($this->purchases as $purchase) {
$purchase->getProduct()->didPurchaseProduct($purchase);
}
$this->setStatus(self::STATUS_PURCHASED)->save();
return $this;
}
public function didFailCharge(PhortuneCharge $charge) {
$charge->setStatus(PhortuneCharge::STATUS_FAILED);
$this->openTransaction();
$this->beginReadLocking();
$copy = clone $this;
$copy->reload();
- if ($copy->getStatus() !== self::STATUS_PURCHASING) {
+ if (($copy->getStatus() !== self::STATUS_PURCHASING) &&
+ ($copy->getStatus() !== self::STATUS_HOLD)) {
throw new Exception(
pht(
- 'Cart has wrong status ("%s") to call didFailCharge(), '.
- 'expected "%s".',
- $copy->getStatus(),
- self::STATUS_PURCHASING));
+ 'Cart has wrong status ("%s") to call didFailCharge().',
+ $copy->getStatus()));
}
$charge->save();
// Move the cart back into STATUS_READY so the user can try
// making the purchase again.
$this->setStatus(self::STATUS_READY)->save();
$this->endReadLocking();
$this->saveTransaction();
return $this;
}
public function willRefundCharge(
PhabricatorUser $actor,
PhortunePaymentProvider $provider,
PhortuneCharge $charge,
PhortuneCurrency $amount) {
if (!$amount->isPositive()) {
throw new Exception(
pht('Trying to refund nonpositive amount of money!'));
}
if ($amount->isGreaterThan($charge->getAmountRefundableAsCurrency())) {
throw new Exception(
pht('Trying to refund more money than remaining on charge!'));
}
if ($charge->getRefundedChargePHID()) {
throw new Exception(
pht('Trying to refund a refund!'));
}
if (($charge->getStatus() !== PhortuneCharge::STATUS_CHARGED) &&
($charge->getStatus() !== PhortuneCharge::STATUS_HOLD)) {
throw new Exception(
pht('Trying to refund an uncharged charge!'));
}
$refund_charge = PhortuneCharge::initializeNewCharge()
->setAccountPHID($this->getAccount()->getPHID())
->setCartPHID($this->getPHID())
->setAuthorPHID($actor->getPHID())
->setMerchantPHID($this->getMerchant()->getPHID())
->setProviderPHID($provider->getProviderConfig()->getPHID())
->setPaymentMethodPHID($charge->getPaymentMethodPHID())
->setRefundedChargePHID($charge->getPHID())
->setAmountAsCurrency($amount->negate());
$charge->openTransaction();
$charge->beginReadLocking();
$copy = clone $charge;
$copy->reload();
if ($copy->getRefundingPHID() !== null) {
throw new Exception(
pht('Trying to refund a charge which is already refunding!'));
}
$refund_charge->save();
$charge->setRefundingPHID($refund_charge->getPHID());
$charge->save();
$charge->endReadLocking();
$charge->saveTransaction();
return $refund_charge;
}
public function didRefundCharge(
PhortuneCharge $charge,
PhortuneCharge $refund) {
$refund->setStatus(PhortuneCharge::STATUS_CHARGED);
$this->openTransaction();
$this->beginReadLocking();
$copy = clone $charge;
$copy->reload();
if ($charge->getRefundingPHID() !== $refund->getPHID()) {
throw new Exception(
pht('Charge is in the wrong refunding state!'));
}
$charge->setRefundingPHID(null);
// NOTE: There's some trickiness here to get the signs right. Both
// these values are positive but the refund has a negative value.
$total_refunded = $charge
->getAmountRefundedAsCurrency()
->add($refund->getAmountAsCurrency()->negate());
$charge->setAmountRefundedAsCurrency($total_refunded);
$charge->save();
$refund->save();
$this->endReadLocking();
$this->saveTransaction();
foreach ($this->purchases as $purchase) {
$purchase->getProduct()->didRefundProduct($purchase);
}
return $this;
}
public function didFailRefund(
PhortuneCharge $charge,
PhortuneCharge $refund) {
$refund->setStatus(PhortuneCharge::STATUS_FAILED);
$this->openTransaction();
$this->beginReadLocking();
$copy = clone $charge;
$copy->reload();
if ($charge->getRefundingPHID() !== $refund->getPHID()) {
throw new Exception(
pht('Charge is in the wrong refunding state!'));
}
$charge->setRefundingPHID(null);
$charge->save();
$refund->save();
$this->endReadLocking();
$this->saveTransaction();
}
public function getName() {
return $this->getImplementation()->getName($this);
}
public function getDoneURI() {
return $this->getImplementation()->getDoneURI($this);
}
public function getCancelURI() {
return $this->getImplementation()->getCancelURI($this);
}
public function getDetailURI() {
return '/phortune/cart/'.$this->getID().'/';
}
public function getCheckoutURI() {
return '/phortune/cart/'.$this->getID().'/checkout/';
}
public function canCancelOrder() {
try {
$this->assertCanCancelOrder();
return true;
} catch (Exception $ex) {
return false;
}
}
public function canRefundOrder() {
try {
$this->assertCanRefundOrder();
return true;
} catch (Exception $ex) {
return false;
}
}
public function assertCanCancelOrder() {
switch ($this->getStatus()) {
case self::STATUS_BUILDING:
throw new Exception(
pht(
'This order can not be cancelled because the application has not '.
'finished building it yet.'));
case self::STATUS_READY:
throw new Exception(
pht(
'This order can not be cancelled because it has not been placed.'));
}
return $this->getImplementation()->assertCanCancelOrder($this);
}
public function assertCanRefundOrder() {
switch ($this->getStatus()) {
case self::STATUS_BUILDING:
throw new Exception(
pht(
'This order can not be refunded because the application has not '.
'finished building it yet.'));
case self::STATUS_READY:
throw new Exception(
pht(
'This order can not be refunded because it has not been placed.'));
}
return $this->getImplementation()->assertCanRefundOrder($this);
}
public function getConfiguration() {
return array(
self::CONFIG_AUX_PHID => true,
self::CONFIG_SERIALIZATION => array(
'metadata' => self::SERIALIZATION_JSON,
),
self::CONFIG_COLUMN_SCHEMA => array(
'status' => 'text32',
'cartClass' => 'text128',
),
self::CONFIG_KEY_SCHEMA => array(
'key_account' => array(
'columns' => array('accountPHID'),
),
'key_merchant' => array(
'columns' => array('merchantPHID'),
),
),
) + parent::getConfiguration();
}
public function generatePHID() {
return PhabricatorPHID::generateNewPHID(
PhortuneCartPHIDType::TYPECONST);
}
public function attachPurchases(array $purchases) {
assert_instances_of($purchases, 'PhortunePurchase');
$this->purchases = $purchases;
return $this;
}
public function getPurchases() {
return $this->assertAttached($this->purchases);
}
public function attachAccount(PhortuneAccount $account) {
$this->account = $account;
return $this;
}
public function getAccount() {
return $this->assertAttached($this->account);
}
public function attachMerchant(PhortuneMerchant $merchant) {
$this->merchant = $merchant;
return $this;
}
public function getMerchant() {
return $this->assertAttached($this->merchant);
}
public function attachImplementation(
PhortuneCartImplementation $implementation) {
$this->implementation = $implementation;
return $this;
}
public function getImplementation() {
return $this->assertAttached($this->implementation);
}
public function getTotalPriceAsCurrency() {
$prices = array();
foreach ($this->getPurchases() as $purchase) {
$prices[] = $purchase->getTotalPriceAsCurrency();
}
return PhortuneCurrency::newFromList($prices);
}
public function setMetadataValue($key, $value) {
$this->metadata[$key] = $value;
return $this;
}
public function getMetadataValue($key, $default = null) {
return idx($this->metadata, $key, $default);
}
/* -( PhabricatorPolicyInterface )----------------------------------------- */
public function getCapabilities() {
return array(
PhabricatorPolicyCapability::CAN_VIEW,
PhabricatorPolicyCapability::CAN_EDIT,
);
}
public function getPolicy($capability) {
// NOTE: Both view and edit use the account's edit policy. We punch a hole
// through this for merchants, below.
return $this
->getAccount()
->getPolicy(PhabricatorPolicyCapability::CAN_EDIT);
}
public function hasAutomaticCapability($capability, PhabricatorUser $viewer) {
if ($this->getAccount()->hasAutomaticCapability($capability, $viewer)) {
return true;
}
// If the viewer controls the merchant this order was placed with, they
// can view the order.
if ($capability == PhabricatorPolicyCapability::CAN_VIEW) {
$can_admin = PhabricatorPolicyFilter::hasCapability(
$viewer,
$this->getMerchant(),
PhabricatorPolicyCapability::CAN_EDIT);
if ($can_admin) {
return true;
}
}
return false;
}
public function describeAutomaticCapability($capability) {
return array(
pht('Orders inherit the policies of the associated account.'),
pht('The merchant you placed an order with can review and manage it.'),
);
}
}
diff --git a/src/applications/phortune/storage/PhortuneCharge.php b/src/applications/phortune/storage/PhortuneCharge.php
index 1b3390fef8..48fb5bc09b 100644
--- a/src/applications/phortune/storage/PhortuneCharge.php
+++ b/src/applications/phortune/storage/PhortuneCharge.php
@@ -1,176 +1,180 @@
<?php
/**
* A charge is a charge (or credit) against an account and represents an actual
* transfer of funds. Each charge is normally associated with a cart, but a
* cart may have multiple charges. For example, a product may have a failed
* charge followed by a successful charge.
*/
final class PhortuneCharge extends PhortuneDAO
implements PhabricatorPolicyInterface {
const STATUS_CHARGING = 'charge:charging';
const STATUS_CHARGED = 'charge:charged';
const STATUS_HOLD = 'charge:hold';
const STATUS_FAILED = 'charge:failed';
protected $accountPHID;
protected $authorPHID;
protected $cartPHID;
protected $providerPHID;
protected $merchantPHID;
protected $paymentMethodPHID;
protected $amountAsCurrency;
protected $amountRefundedAsCurrency;
protected $refundedChargePHID;
protected $refundingPHID;
protected $status;
protected $metadata = array();
private $account = self::ATTACHABLE;
private $cart = self::ATTACHABLE;
public static function initializeNewCharge() {
return id(new PhortuneCharge())
->setStatus(self::STATUS_CHARGING)
->setAmountRefundedAsCurrency(PhortuneCurrency::newEmptyCurrency());
}
public function getConfiguration() {
return array(
self::CONFIG_AUX_PHID => true,
self::CONFIG_SERIALIZATION => array(
'metadata' => self::SERIALIZATION_JSON,
),
self::CONFIG_APPLICATION_SERIALIZERS => array(
'amountAsCurrency' => new PhortuneCurrencySerializer(),
'amountRefundedAsCurrency' => new PhortuneCurrencySerializer(),
),
self::CONFIG_COLUMN_SCHEMA => array(
'paymentMethodPHID' => 'phid?',
'refundedChargePHID' => 'phid?',
'refundingPHID' => 'phid?',
'amountAsCurrency' => 'text64',
'amountRefundedAsCurrency' => 'text64',
'status' => 'text32',
),
self::CONFIG_KEY_SCHEMA => array(
'key_cart' => array(
'columns' => array('cartPHID'),
),
'key_account' => array(
'columns' => array('accountPHID'),
),
'key_merchant' => array(
'columns' => array('merchantPHID'),
),
'key_provider' => array(
'columns' => array('providerPHID'),
),
),
) + parent::getConfiguration();
}
public static function getStatusNameMap() {
return array(
self::STATUS_CHARGING => pht('Charging'),
self::STATUS_CHARGED => pht('Charged'),
self::STATUS_HOLD => pht('Hold'),
self::STATUS_FAILED => pht('Failed'),
);
}
public static function getNameForStatus($status) {
return idx(self::getStatusNameMap(), $status, pht('Unknown'));
}
+ public function isRefund() {
+ return $this->getAmountAsCurrency()->negate()->isPositive();
+ }
+
public function getStatusForDisplay() {
if ($this->getStatus() == self::STATUS_CHARGED) {
if ($this->getRefundedChargePHID()) {
return pht('Refund');
}
$refunded = $this->getAmountRefundedAsCurrency();
if ($refunded->isPositive()) {
if ($refunded->isEqualTo($this->getAmountAsCurrency())) {
return pht('Fully Refunded');
} else {
return pht('%s Refunded', $refunded->formatForDisplay());
}
}
}
return self::getNameForStatus($this->getStatus());
}
public function generatePHID() {
return PhabricatorPHID::generateNewPHID(
PhortuneChargePHIDType::TYPECONST);
}
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 getAccount() {
return $this->assertAttached($this->account);
}
public function attachAccount(PhortuneAccount $account) {
$this->account = $account;
return $this;
}
public function getCart() {
return $this->assertAttached($this->cart);
}
public function attachCart(PhortuneCart $cart = null) {
$this->cart = $cart;
return $this;
}
public function getAmountRefundableAsCurrency() {
$amount = $this->getAmountAsCurrency();
$refunded = $this->getAmountRefundedAsCurrency();
// We can't refund negative amounts of money, since it does not make
// sense and is not possible in the various payment APIs.
$refundable = $amount->subtract($refunded);
if ($refundable->isPositive()) {
return $refundable;
} else {
return PhortuneCurrency::newEmptyCurrency();
}
}
/* -( PhabricatorPolicyInterface )----------------------------------------- */
public function getCapabilities() {
return array(
PhabricatorPolicyCapability::CAN_VIEW,
);
}
public function getPolicy($capability) {
return $this->getAccount()->getPolicy($capability);
}
public function hasAutomaticCapability($capability, PhabricatorUser $viewer) {
return $this->getAccount()->hasAutomaticCapability($capability, $viewer);
}
public function describeAutomaticCapability($capability) {
return pht('Charges inherit the policies of the associated account.');
}
}

File Metadata

Mime Type
text/x-diff
Expires
Mon, Mar 16, 11:47 PM (1 d, 20 h)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
963654
Default Alt Text
(105 KB)

Event Timeline