diff --git a/src/applications/fund/phortune/FundBackerProduct.php b/src/applications/fund/phortune/FundBackerProduct.php --- a/src/applications/fund/phortune/FundBackerProduct.php +++ b/src/applications/fund/phortune/FundBackerProduct.php @@ -79,6 +79,8 @@ 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()) diff --git a/src/applications/phortune/controller/PhortuneAccountViewController.php b/src/applications/phortune/controller/PhortuneAccountViewController.php --- a/src/applications/phortune/controller/PhortuneAccountViewController.php +++ b/src/applications/phortune/controller/PhortuneAccountViewController.php @@ -169,6 +169,8 @@ ->withStatuses( array( PhortuneCart::STATUS_PURCHASING, + PhortuneCart::STATUS_CHARGED, + PhortuneCart::STATUS_HOLD, PhortuneCart::STATUS_PURCHASED, )) ->execute(); @@ -197,6 +199,7 @@ $rowc[] = ''; $rows[] = array( + $cart->getID(), phutil_tag( 'strong', array(), @@ -206,6 +209,7 @@ 'strong', array(), $cart->getTotalPriceAsCurrency()->formatForDisplay()), + PhortuneCart::getNameForStatus($cart->getStatus()), phabricator_datetime($cart->getDateModified(), $viewer), ); foreach ($purchases as $purchase) { @@ -219,6 +223,7 @@ $handles[$purchase->getPHID()]->renderLink(), $price, '', + '', ); } } @@ -227,21 +232,25 @@ ->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) diff --git a/src/applications/phortune/controller/PhortuneCartUpdateController.php b/src/applications/phortune/controller/PhortuneCartUpdateController.php --- a/src/applications/phortune/controller/PhortuneCartUpdateController.php +++ b/src/applications/phortune/controller/PhortuneCartUpdateController.php @@ -22,7 +22,41 @@ 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 --- a/src/applications/phortune/controller/PhortuneCartViewController.php +++ b/src/applications/phortune/controller/PhortuneCartViewController.php @@ -83,8 +83,7 @@ $header = id(new PHUIHeaderView()) ->setUser($viewer) - ->setHeader(pht('Order Detail')) - ->setPolicyObject($cart); + ->setHeader(pht('Order Detail')); $cart_box = id(new PHUIObjectBoxView()) ->setHeader($header) diff --git a/src/applications/phortune/provider/PhortuneBalancedPaymentProvider.php b/src/applications/phortune/provider/PhortuneBalancedPaymentProvider.php --- a/src/applications/phortune/provider/PhortuneBalancedPaymentProvider.php +++ b/src/applications/phortune/provider/PhortuneBalancedPaymentProvider.php @@ -102,10 +102,7 @@ } 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 @@ 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 @@ 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 @@ $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 @@ 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 @@ 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 --- a/src/applications/phortune/provider/PhortunePayPalPaymentProvider.php +++ b/src/applications/phortune/provider/PhortunePayPalPaymentProvider.php @@ -192,6 +192,62 @@ $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 @@ '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, diff --git a/src/applications/phortune/provider/PhortunePaymentProvider.php b/src/applications/phortune/provider/PhortunePaymentProvider.php --- a/src/applications/phortune/provider/PhortunePaymentProvider.php +++ b/src/applications/phortune/provider/PhortunePaymentProvider.php @@ -149,7 +149,9 @@ abstract protected function executeRefund( PhortuneCharge $charge, - PhortuneCharge $charge); + PhortuneCharge $refund); + + abstract public function updateCharge(PhortuneCharge $charge); /* -( Adding Payment Methods )--------------------------------------------- */ diff --git a/src/applications/phortune/provider/PhortuneStripePaymentProvider.php b/src/applications/phortune/provider/PhortuneStripePaymentProvider.php --- a/src/applications/phortune/provider/PhortuneStripePaymentProvider.php +++ b/src/applications/phortune/provider/PhortuneStripePaymentProvider.php @@ -116,8 +116,7 @@ } 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 @@ 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 @@ protected function executeRefund( PhortuneCharge $charge, PhortuneCharge $refund) { + $this->loadStripeAPILibraries(); $charge_id = $charge->getMetadataValue('stripe.chargeID'); if (!$charge_id) { @@ -167,9 +165,6 @@ 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 @@ $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 @@ 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 @@ 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 --- a/src/applications/phortune/provider/PhortuneTestPaymentProvider.php +++ b/src/applications/phortune/provider/PhortuneTestPaymentProvider.php @@ -62,6 +62,10 @@ return; } + public function updateCharge(PhortuneCharge $charge) { + return; + } + public function getAllConfigurableProperties() { return array(); } diff --git a/src/applications/phortune/provider/PhortuneWePayPaymentProvider.php b/src/applications/phortune/provider/PhortuneWePayPaymentProvider.php --- a/src/applications/phortune/provider/PhortuneWePayPaymentProvider.php +++ b/src/applications/phortune/provider/PhortuneWePayPaymentProvider.php @@ -49,8 +49,7 @@ } 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 @@ 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 @@ $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 @@ public function processControllerRequest( PhortuneProviderActionController $controller, AphrontRequest $request) { + $wepay = $this->loadWePayAPILibraries(); $viewer = $request->getUser(); @@ -244,15 +248,6 @@ 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 @@ 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 --- a/src/applications/phortune/storage/PhortuneCart.php +++ b/src/applications/phortune/storage/PhortuneCart.php @@ -149,13 +149,12 @@ $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 @@ $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(); diff --git a/src/applications/phortune/storage/PhortuneCharge.php b/src/applications/phortune/storage/PhortuneCharge.php --- a/src/applications/phortune/storage/PhortuneCharge.php +++ b/src/applications/phortune/storage/PhortuneCharge.php @@ -84,6 +84,10 @@ 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()) {