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, '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->getCheckoutURI()); } 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())); }
/** * @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(); } $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, 'auto_capture' => true, 'require_shipping' => 0, 'shipping_fee' => 0, 'charge_tax' => 0, 'mode' => 'regular', '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->getCheckoutURI()); 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())); }