1
0
Fork 0
mirror of https://we.phorge.it/source/phorge.git synced 2024-12-03 12:12:43 +01:00

Implement Phortune charge updates

Summary: Ref T2787. These don't necessarily do a ton yet, but we can get PayPal out of hold, at least.

Test Plan: Updated charges from all providers. Cleared a PayPal hold.

Reviewers: btrahan

Reviewed By: btrahan

Subscribers: epriestley

Maniphest Tasks: T2787

Differential Revision: https://secure.phabricator.com/D10670
This commit is contained in:
epriestley 2014-10-09 16:57:52 -07:00
parent 2a2fb62229
commit f41ae2228a
12 changed files with 214 additions and 68 deletions

View file

@ -79,6 +79,8 @@ final class FundBackerProduct extends PhortuneProductImplementation {
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())

View file

@ -169,6 +169,8 @@ final class PhortuneAccountViewController extends PhortuneController {
->withStatuses(
array(
PhortuneCart::STATUS_PURCHASING,
PhortuneCart::STATUS_CHARGED,
PhortuneCart::STATUS_HOLD,
PhortuneCart::STATUS_PURCHASED,
))
->execute();
@ -197,6 +199,7 @@ final class PhortuneAccountViewController extends PhortuneController {
$rowc[] = '';
$rows[] = array(
$cart->getID(),
phutil_tag(
'strong',
array(),
@ -206,6 +209,7 @@ final class PhortuneAccountViewController extends PhortuneController {
'strong',
array(),
$cart->getTotalPriceAsCurrency()->formatForDisplay()),
PhortuneCart::getNameForStatus($cart->getStatus()),
phabricator_datetime($cart->getDateModified(), $viewer),
);
foreach ($purchases as $purchase) {
@ -219,6 +223,7 @@ final class PhortuneAccountViewController extends PhortuneController {
$handles[$purchase->getPHID()]->renderLink(),
$price,
'',
'',
);
}
}
@ -227,21 +232,25 @@ final class PhortuneAccountViewController extends PhortuneController {
->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)

View file

@ -22,7 +22,41 @@ final class PhortuneCartUpdateController
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());

View file

@ -83,8 +83,7 @@ final class PhortuneCartViewController
$header = id(new PHUIHeaderView())
->setUser($viewer)
->setHeader(pht('Order Detail'))
->setPolicyObject($cart);
->setHeader(pht('Order Detail'));
$cart_box = id(new PHUIObjectBoxView())
->setHeader($header)

View file

@ -102,10 +102,7 @@ final class PhortuneBalancedPaymentProvider extends PhortunePaymentProvider {
}
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.
@ -140,11 +137,7 @@ final class PhortuneBalancedPaymentProvider extends PhortunePaymentProvider {
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();
@ -182,11 +175,7 @@ final class PhortuneBalancedPaymentProvider extends PhortunePaymentProvider {
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) {
@ -214,6 +203,24 @@ final class PhortuneBalancedPaymentProvider extends PhortunePaymentProvider {
$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()
@ -255,14 +262,10 @@ final class PhortuneBalancedPaymentProvider extends PhortunePaymentProvider {
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;
@ -357,4 +360,11 @@ final class PhortuneBalancedPaymentProvider extends PhortunePaymentProvider {
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';
}
}

View file

@ -192,6 +192,62 @@ final class PhortunePayPalPaymentProvider extends PhortunePaymentProvider {
$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()
@ -278,6 +334,7 @@ final class PhortunePayPalPaymentProvider extends PhortunePaymentProvider {
'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,

View file

@ -149,7 +149,9 @@ abstract class PhortunePaymentProvider {
abstract protected function executeRefund(
PhortuneCharge $charge,
PhortuneCharge $charge);
PhortuneCharge $refund);
abstract public function updateCharge(PhortuneCharge $charge);
/* -( Adding Payment Methods )--------------------------------------------- */

View file

@ -116,8 +116,7 @@ final class PhortuneStripePaymentProvider extends PhortunePaymentProvider {
}
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);
@ -131,9 +130,7 @@ final class PhortuneStripePaymentProvider extends PhortunePaymentProvider {
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();
@ -160,6 +157,7 @@ final class PhortuneStripePaymentProvider extends PhortunePaymentProvider {
protected function executeRefund(
PhortuneCharge $charge,
PhortuneCharge $refund) {
$this->loadStripeAPILibraries();
$charge_id = $charge->getMetadataValue('stripe.chargeID');
if (!$charge_id) {
@ -167,9 +165,6 @@ final class PhortuneStripePaymentProvider extends PhortunePaymentProvider {
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()
@ -192,6 +187,22 @@ final class PhortuneStripePaymentProvider extends PhortunePaymentProvider {
$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()
@ -221,12 +232,10 @@ final class PhortuneStripePaymentProvider extends PhortunePaymentProvider {
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'];
@ -362,4 +371,9 @@ final class PhortuneStripePaymentProvider extends PhortunePaymentProvider {
return null;
}
private function loadStripeAPILibraries() {
$root = dirname(phutil_get_library_root('phabricator'));
require_once $root.'/externals/stripe-php/lib/Stripe.php';
}
}

View file

@ -62,6 +62,10 @@ final class PhortuneTestPaymentProvider extends PhortunePaymentProvider {
return;
}
public function updateCharge(PhortuneCharge $charge) {
return;
}
public function getAllConfigurableProperties() {
return array();
}

View file

@ -49,8 +49,7 @@ final class PhortuneWePayPaymentProvider extends PhortunePaymentProvider {
}
public function runConfigurationTest() {
$root = dirname(phutil_get_library_root('phabricator'));
require_once $root.'/externals/wepay/wepay.php';
$this->loadWePayAPILibraries();
WePay::useStaging(
$this->getWePayClientID(),
@ -189,20 +188,12 @@ final class PhortuneWePayPaymentProvider extends PhortunePaymentProvider {
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(),
);
@ -210,6 +201,18 @@ final class PhortuneWePayPaymentProvider extends PhortunePaymentProvider {
$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() {
@ -236,6 +239,7 @@ final class PhortuneWePayPaymentProvider extends PhortunePaymentProvider {
public function processControllerRequest(
PhortuneProviderActionController $controller,
AphrontRequest $request) {
$wepay = $this->loadWePayAPILibraries();
$viewer = $request->getUser();
@ -244,15 +248,6 @@ final class PhortuneWePayPaymentProvider extends PhortunePaymentProvider {
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':
@ -388,5 +383,23 @@ final class PhortuneWePayPaymentProvider extends PhortunePaymentProvider {
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;
}
}

View file

@ -149,13 +149,12 @@ final class PhortuneCart extends PhortuneDAO
$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();
@ -182,13 +181,12 @@ final class PhortuneCart extends PhortuneDAO
$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();

View file

@ -84,6 +84,10 @@ final class PhortuneCharge extends PhortuneDAO
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()) {