diff --git a/resources/sql/autopatches/20140721.phortune.4.cartstatus.sql b/resources/sql/autopatches/20140721.phortune.4.cartstatus.sql new file mode 100644 index 0000000000..fc18eb78dc --- /dev/null +++ b/resources/sql/autopatches/20140721.phortune.4.cartstatus.sql @@ -0,0 +1,2 @@ +ALTER TABLE {$NAMESPACE}_phortune.phortune_cart + ADD status VARCHAR(32) NOT NULL COLLATE utf8_bin; diff --git a/resources/sql/autopatches/20140721.phortune.5.cstatusdefault.sql b/resources/sql/autopatches/20140721.phortune.5.cstatusdefault.sql new file mode 100644 index 0000000000..1232164f46 --- /dev/null +++ b/resources/sql/autopatches/20140721.phortune.5.cstatusdefault.sql @@ -0,0 +1,2 @@ +UPDATE {$NAMESPACE}_phortune.phortune_cart + SET status = 'cart:ready' WHERE status = ''; diff --git a/resources/sql/autopatches/20140721.phortune.6.onetimecharge.sql b/resources/sql/autopatches/20140721.phortune.6.onetimecharge.sql new file mode 100644 index 0000000000..4152efb0d0 --- /dev/null +++ b/resources/sql/autopatches/20140721.phortune.6.onetimecharge.sql @@ -0,0 +1,3 @@ +ALTER TABLE {$NAMESPACE}_phortune.phortune_charge + ADD paymentProviderKey VARCHAR(128) NOT NULL COLLATE utf8_bin + AFTER cartPHID; diff --git a/resources/sql/autopatches/20140721.phortune.7.nullmethod.sql b/resources/sql/autopatches/20140721.phortune.7.nullmethod.sql new file mode 100644 index 0000000000..d843251bb0 --- /dev/null +++ b/resources/sql/autopatches/20140721.phortune.7.nullmethod.sql @@ -0,0 +1,4 @@ +/* Make this nullable to support one-time providers. */ + +ALTER TABLE {$NAMESPACE}_phortune.phortune_charge + CHANGE paymentMethodPHID paymentMethodPHID VARCHAR(64) COLLATE utf8_bin; diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php index 9883926b7a..2f753ed5c4 100644 --- a/src/__phutil_library_map__.php +++ b/src/__phutil_library_map__.php @@ -2488,7 +2488,6 @@ phutil_register_library_map(array( 'PholioTransactionView' => 'applications/pholio/view/PholioTransactionView.php', 'PholioUploadedImageView' => 'applications/pholio/view/PholioUploadedImageView.php', 'PhortuneAccount' => 'applications/phortune/storage/PhortuneAccount.php', - 'PhortuneAccountBuyController' => 'applications/phortune/controller/PhortuneAccountBuyController.php', 'PhortuneAccountEditor' => 'applications/phortune/editor/PhortuneAccountEditor.php', 'PhortuneAccountQuery' => 'applications/phortune/query/PhortuneAccountQuery.php', 'PhortuneAccountTransaction' => 'applications/phortune/storage/PhortuneAccountTransaction.php', @@ -2496,7 +2495,10 @@ phutil_register_library_map(array( 'PhortuneAccountViewController' => 'applications/phortune/controller/PhortuneAccountViewController.php', 'PhortuneBalancedPaymentProvider' => 'applications/phortune/provider/PhortuneBalancedPaymentProvider.php', 'PhortuneCart' => 'applications/phortune/storage/PhortuneCart.php', + 'PhortuneCartCheckoutController' => 'applications/phortune/controller/PhortuneCartCheckoutController.php', + 'PhortuneCartController' => 'applications/phortune/controller/PhortuneCartController.php', 'PhortuneCartQuery' => 'applications/phortune/query/PhortuneCartQuery.php', + 'PhortuneCartViewController' => 'applications/phortune/controller/PhortuneCartViewController.php', 'PhortuneCharge' => 'applications/phortune/storage/PhortuneCharge.php', 'PhortuneChargeQuery' => 'applications/phortune/query/PhortuneChargeQuery.php', 'PhortuneConstants' => 'applications/phortune/constants/PhortuneConstants.php', @@ -5371,7 +5373,6 @@ phutil_register_library_map(array( 'PhortuneDAO', 'PhabricatorPolicyInterface', ), - 'PhortuneAccountBuyController' => 'PhortuneController', 'PhortuneAccountEditor' => 'PhabricatorApplicationTransactionEditor', 'PhortuneAccountQuery' => 'PhabricatorCursorPagedPolicyAwareQuery', 'PhortuneAccountTransaction' => 'PhabricatorApplicationTransaction', @@ -5382,7 +5383,10 @@ phutil_register_library_map(array( 'PhortuneDAO', 'PhabricatorPolicyInterface', ), + 'PhortuneCartCheckoutController' => 'PhortuneCartController', + 'PhortuneCartController' => 'PhortuneController', 'PhortuneCartQuery' => 'PhabricatorCursorPagedPolicyAwareQuery', + 'PhortuneCartViewController' => 'PhortuneCartController', 'PhortuneCharge' => array( 'PhortuneDAO', 'PhabricatorPolicyInterface', diff --git a/src/applications/phortune/application/PhabricatorPhortuneApplication.php b/src/applications/phortune/application/PhabricatorPhortuneApplication.php index 64df95fd4b..d2eb2338e0 100644 --- a/src/applications/phortune/application/PhabricatorPhortuneApplication.php +++ b/src/applications/phortune/application/PhabricatorPhortuneApplication.php @@ -41,7 +41,10 @@ final class PhabricatorPhortuneApplication extends PhabricatorApplication { ), 'buy/(?P\d+)/' => 'PhortuneProductPurchaseController', ), - 'cart/(?P\d+)/' => 'PhortuneAccountBuyController', + 'cart/(?P\d+)/' => array( + '' => 'PhortuneCartViewController', + 'checkout/' => 'PhortuneCartCheckoutController', + ), 'account/' => array( '' => 'PhortuneAccountListController', 'edit/(?:(?P\d+)/)?' => 'PhortuneAccountEditController', diff --git a/src/applications/phortune/controller/PhortuneAccountViewController.php b/src/applications/phortune/controller/PhortuneAccountViewController.php index 1124399d10..1914a3ef41 100644 --- a/src/applications/phortune/controller/PhortuneAccountViewController.php +++ b/src/applications/phortune/controller/PhortuneAccountViewController.php @@ -152,45 +152,7 @@ final class PhortuneAccountViewController extends PhortuneController { ->withAccountPHIDs(array($account->getPHID())) ->execute(); - $rows = array(); - foreach ($charges as $charge) { - $rows[] = array( - $charge->getID(), - $charge->getCartPHID(), - $charge->getPaymentMethodPHID(), - PhortuneCurrency::newFromUSDCents($charge->getAmountInCents()) - ->formatForDisplay(), - $charge->getStatus(), - phabricator_datetime($charge->getDateCreated(), $viewer), - ); - } - - $charge_table = id(new AphrontTableView($rows)) - ->setHeaders( - array( - pht('Charge ID'), - pht('Cart'), - pht('Method'), - pht('Amount'), - pht('Status'), - pht('Created'), - )) - ->setColumnClasses( - array( - '', - '', - '', - 'wide right', - '', - '', - )); - - $header = id(new PHUIHeaderView()) - ->setHeader(pht('Charge History')); - - return id(new PHUIObjectBoxView()) - ->setHeader($header) - ->appendChild($charge_table); + return $this->buildChargesTable($charges); } private function buildAccountHistorySection(PhortuneAccount $account) { diff --git a/src/applications/phortune/controller/PhortuneAccountBuyController.php b/src/applications/phortune/controller/PhortuneCartCheckoutController.php similarity index 76% rename from src/applications/phortune/controller/PhortuneAccountBuyController.php rename to src/applications/phortune/controller/PhortuneCartCheckoutController.php index e279a84d41..bd2738e73d 100644 --- a/src/applications/phortune/controller/PhortuneAccountBuyController.php +++ b/src/applications/phortune/controller/PhortuneCartCheckoutController.php @@ -1,7 +1,7 @@ openTransaction(); $charge->save(); - // TODO: We should be setting some kind of status on the cart here. + $cart->setStatus(PhortuneCart::STATUS_PURCHASING); $cart->save(); $charge->saveTransaction(); $provider->applyCharge($method, $charge); - throw new Exception('Executed a charge! Your money is gone forever!'); + $cart->setStatus(PhortuneCart::STATUS_PURCHASED); + $cart->save(); + + $view_uri = $this->getApplicationURI('cart/'.$cart->getID().'/'); + + return id(new AphrontRedirectResponse())->setURI($view_uri); } } - - $rows = array(); - $total = 0; - foreach ($cart->getPurchases() as $purchase) { - $rows[] = array( - pht('A Purchase'), - PhortuneCurrency::newFromUSDCents($purchase->getBasePriceInCents()) - ->formatForDisplay(), - $purchase->getQuantity(), - PhortuneCurrency::newFromUSDCents($purchase->getTotalPriceInCents()) - ->formatForDisplay(), - ); - - $total += $purchase->getTotalPriceInCents(); - } - - $rows[] = array( - phutil_tag('strong', array(), pht('Total')), - '', - '', - phutil_tag('strong', array(), - PhortuneCurrency::newFromUSDCents($total)->formatForDisplay()), - ); - - $table = new AphrontTableView($rows); - $table->setHeaders( - array( - pht('Item'), - pht('Price'), - pht('Qty.'), - pht('Total'), - )); - $table->setColumnClasses( - array( - 'wide', - 'right', - 'right', - 'right', - )); - - $cart_box = id(new PHUIObjectBoxView()) - ->setHeaderText(pht('Your Cart')) - ->setFormErrors($errors) - ->appendChild($table); + $cart_box = $this->buildCartContents($cart); + $cart_box->setFormErrors($errors); $title = pht('Buy Stuff'); diff --git a/src/applications/phortune/controller/PhortuneCartController.php b/src/applications/phortune/controller/PhortuneCartController.php new file mode 100644 index 0000000000..36d00d789d --- /dev/null +++ b/src/applications/phortune/controller/PhortuneCartController.php @@ -0,0 +1,52 @@ +getPurchases() as $purchase) { + $rows[] = array( + pht('A Purchase'), + PhortuneCurrency::newFromUSDCents($purchase->getBasePriceInCents()) + ->formatForDisplay(), + $purchase->getQuantity(), + PhortuneCurrency::newFromUSDCents($purchase->getTotalPriceInCents()) + ->formatForDisplay(), + ); + + $total += $purchase->getTotalPriceInCents(); + } + + $rows[] = array( + phutil_tag('strong', array(), pht('Total')), + '', + '', + phutil_tag('strong', array(), + PhortuneCurrency::newFromUSDCents($total)->formatForDisplay()), + ); + + $table = new AphrontTableView($rows); + $table->setHeaders( + array( + pht('Item'), + pht('Price'), + pht('Qty.'), + pht('Total'), + )); + $table->setColumnClasses( + array( + 'wide', + 'right', + 'right', + 'right', + )); + + return id(new PHUIObjectBoxView()) + ->setHeaderText(pht('Cart Contents')) + ->appendChild($table); + } + +} diff --git a/src/applications/phortune/controller/PhortuneCartViewController.php b/src/applications/phortune/controller/PhortuneCartViewController.php new file mode 100644 index 0000000000..20265a902c --- /dev/null +++ b/src/applications/phortune/controller/PhortuneCartViewController.php @@ -0,0 +1,50 @@ +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(); + } + + $cart_box = $this->buildCartContents($cart); + + $charges = id(new PhortuneChargeQuery()) + ->setViewer($viewer) + ->withCartPHIDs(array($cart->getPHID())) + ->execute(); + + $charges_table = $this->buildChargesTable($charges); + + $account = $cart->getAccount(); + + $crumbs = $this->buildApplicationCrumbs(); + $crumbs->addTextCrumb(pht('Cart')); + + return $this->buildApplicationPage( + array( + $crumbs, + $cart_box, + $charges_table, + ), + array( + 'title' => pht('Cart'), + )); + + } +} diff --git a/src/applications/phortune/controller/PhortuneController.php b/src/applications/phortune/controller/PhortuneController.php index 66ba0e6549..a8285124be 100644 --- a/src/applications/phortune/controller/PhortuneController.php +++ b/src/applications/phortune/controller/PhortuneController.php @@ -52,5 +52,52 @@ abstract class PhortuneController extends PhabricatorController { return $account; } + protected function buildChargesTable(array $charges) { + $request = $this->getRequest(); + $viewer = $request->getUser(); + + $rows = array(); + foreach ($charges as $charge) { + $rows[] = array( + $charge->getID(), + $charge->getCartPHID(), + $charge->getPaymentProviderKey(), + $charge->getPaymentMethodPHID(), + PhortuneCurrency::newFromUSDCents($charge->getAmountInCents()) + ->formatForDisplay(), + $charge->getStatus(), + phabricator_datetime($charge->getDateCreated(), $viewer), + ); + } + + $charge_table = id(new AphrontTableView($rows)) + ->setHeaders( + array( + pht('Charge ID'), + pht('Cart'), + pht('Provider'), + pht('Method'), + pht('Amount'), + pht('Status'), + pht('Created'), + )) + ->setColumnClasses( + array( + '', + '', + '', + '', + 'wide right', + '', + '', + )); + + $header = id(new PHUIHeaderView()) + ->setHeader(pht('Charge History')); + + return id(new PHUIObjectBoxView()) + ->setHeader($header) + ->appendChild($charge_table); + } } diff --git a/src/applications/phortune/controller/PhortuneProductPurchaseController.php b/src/applications/phortune/controller/PhortuneProductPurchaseController.php index 14f8059105..285ca03cfb 100644 --- a/src/applications/phortune/controller/PhortuneProductPurchaseController.php +++ b/src/applications/phortune/controller/PhortuneProductPurchaseController.php @@ -39,6 +39,7 @@ final class PhortuneProductPurchaseController $cart = new PhortuneCart(); $cart->openTransaction(); + $cart->setStatus(PhortuneCart::STATUS_READY); $cart->setAccountPHID($account->getPHID()); $cart->setAuthorPHID($user->getPHID()); $cart->save(); @@ -57,7 +58,8 @@ final class PhortuneProductPurchaseController $cart->saveTransaction(); - $cart_uri = $this->getApplicationURI('/cart/'.$cart->getID().'/'); + $cart_id = $cart->getID(); + $cart_uri = $this->getApplicationURI('/cart/'.$cart_id.'/checkout/'); return id(new AphrontRedirectResponse())->setURI($cart_uri); } diff --git a/src/applications/phortune/controller/PhortuneProviderController.php b/src/applications/phortune/controller/PhortuneProviderController.php index 5389418311..96d2bc4157 100644 --- a/src/applications/phortune/controller/PhortuneProviderController.php +++ b/src/applications/phortune/controller/PhortuneProviderController.php @@ -56,7 +56,19 @@ final class PhortuneProviderController extends PhortuneController { public function loadCart($id) { - return id(new PhortuneCart()); + $request = $this->getRequest(); + $viewer = $request->getUser(); + + return id(new PhortuneCartQuery()) + ->setViewer($viewer) + ->needPurchases(true) + ->withIDs(array($id)) + ->requireCapabilities( + array( + PhabricatorPolicyCapability::CAN_VIEW, + PhabricatorPolicyCapability::CAN_EDIT, + )) + ->executeOne(); } } diff --git a/src/applications/phortune/provider/PhortunePaypalPaymentProvider.php b/src/applications/phortune/provider/PhortunePaypalPaymentProvider.php index 55c9b82b97..ad55a4e32b 100644 --- a/src/applications/phortune/provider/PhortunePaypalPaymentProvider.php +++ b/src/applications/phortune/provider/PhortunePaypalPaymentProvider.php @@ -121,7 +121,7 @@ final class PhortunePaypalPaymentProvider extends PhortunePaymentProvider { 'cartID' => $cart->getID(), )); - $total_in_cents = $cart->getTotalInCents(); + $total_in_cents = $cart->getTotalPriceInCents(); $price = PhortuneCurrency::newFromUSDCents($total_in_cents); $result = $this diff --git a/src/applications/phortune/provider/PhortuneWePayPaymentProvider.php b/src/applications/phortune/provider/PhortuneWePayPaymentProvider.php index b496cf91ce..0295f31256 100644 --- a/src/applications/phortune/provider/PhortuneWePayPaymentProvider.php +++ b/src/applications/phortune/provider/PhortuneWePayPaymentProvider.php @@ -111,11 +111,15 @@ final class PhortuneWePayPaymentProvider extends PhortunePaymentProvider { PhortuneProviderController $controller, AphrontRequest $request) { + $viewer = $request->getUser(); + $cart = $controller->loadCart($request->getInt('cartID')); if (!$cart) { return new Aphront404Response(); } + $cart_uri = '/phortune/cart/'.$cart->getID().'/'; + $root = dirname(phutil_get_library_root('phabricator')); require_once $root.'/externals/wepay/wepay.php'; @@ -139,7 +143,7 @@ final class PhortuneWePayPaymentProvider extends PhortunePaymentProvider { 'cartID' => $cart->getID(), )); - $total_in_cents = $cart->getTotalInCents(); + $total_in_cents = $cart->getTotalPriceInCents(); $price = PhortuneCurrency::newFromUSDCents($total_in_cents); $params = array( @@ -153,7 +157,12 @@ final class PhortuneWePayPaymentProvider extends PhortunePaymentProvider { 'fee_payer' => 'Payee', 'redirect_uri' => $return_uri, 'fallback_uri' => $cancel_uri, - 'auto_capture' => false, + + // 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, @@ -163,18 +172,57 @@ final class PhortuneWePayPaymentProvider extends PhortunePaymentProvider { $result = $wepay->request('checkout/create', $params); - // NOTE: We might want to store "$result->checkout_id" on the Cart. + // TODO: We must store "$result->checkout_id" on the Cart since the + // user might not end up back here. Really this needs a bunch of junk. $uri = new PhutilURI($result->checkout_uri); return id(new AphrontRedirectResponse())->setURI($uri); case 'charge': + $checkout_id = $request->getInt('checkout_id'); + $params = array( + 'checkout_id' => $checkout_id, + ); - // NOTE: We get $_REQUEST['checkout_id'] here, but our parameters are - // dropped so we should stop depending on them or shove them into the - // URI. + $checkout = $wepay->request('checkout', $params); + if ($checkout->reference_id != $cart->getPHID()) { + throw new Exception( + pht('Checkout reference ID does not match cart PHID!')); + } - var_dump($_REQUEST); - break; + switch ($checkout->state) { + case 'authorized': + case 'reserved': + case 'captured': + break; + default: + throw new Exception( + pht( + 'Checkout is in bad state "%s"!', + $result->state)); + } + + $unguarded = AphrontWriteGuard::beginScopedUnguardedWrites(); + + $charge = id(new PhortuneCharge()) + ->setAmountInCents((int)$checkout->gross * 100) + ->setAccountPHID($cart->getAccount()->getPHID()) + ->setAuthorPHID($viewer->getPHID()) + ->setPaymentProviderKey($this->getProviderKey()) + ->setCartPHID($cart->getPHID()) + ->setStatus(PhortuneCharge::STATUS_CHARGING) + ->save(); + + $cart->openTransaction(); + $charge->setStatus(PhortuneCharge::STATUS_CHARGED); + $charge->save(); + + $cart->setStatus(PhortuneCart::STATUS_PURCHASED); + $cart->save(); + $cart->saveTransaction(); + + unset($unguarded); + + return id(new AphrontRedirectResponse())->setURI($cart_uri); case 'cancel': var_dump($_REQUEST); break; diff --git a/src/applications/phortune/query/PhortuneChargeQuery.php b/src/applications/phortune/query/PhortuneChargeQuery.php index 60f2a03921..04ef7eae8b 100644 --- a/src/applications/phortune/query/PhortuneChargeQuery.php +++ b/src/applications/phortune/query/PhortuneChargeQuery.php @@ -6,6 +6,7 @@ final class PhortuneChargeQuery private $ids; private $phids; private $accountPHIDs; + private $cartPHIDs; public function withIDs(array $ids) { $this->ids = $ids; @@ -22,6 +23,11 @@ final class PhortuneChargeQuery return $this; } + public function withCartPHIDs(array $cart_phids) { + $this->cartPHIDs = $cart_phids; + return $this; + } + protected function loadPage() { $table = new PhortuneCharge(); $conn = $table->establishConnection('r'); @@ -83,6 +89,13 @@ final class PhortuneChargeQuery $this->accountPHIDs); } + if ($this->cartPHIDs !== null) { + $where[] = qsprintf( + $conn, + 'charge.cartPHID IN (%Ls)', + $this->cartPHIDs); + } + return $this->formatWhereClause($where); } diff --git a/src/applications/phortune/storage/PhortuneCart.php b/src/applications/phortune/storage/PhortuneCart.php index 1237fb23f2..eee9668bc0 100644 --- a/src/applications/phortune/storage/PhortuneCart.php +++ b/src/applications/phortune/storage/PhortuneCart.php @@ -3,8 +3,13 @@ final class PhortuneCart extends PhortuneDAO implements PhabricatorPolicyInterface { + const STATUS_READY = 'cart:ready'; + const STATUS_PURCHASING = 'cart:purchasing'; + const STATUS_PURCHASED = 'cart:purchased'; + protected $accountPHID; protected $authorPHID; + protected $status; protected $metadata; private $account = self::ATTACHABLE; @@ -24,10 +29,6 @@ final class PhortuneCart extends PhortuneDAO PhabricatorPHIDConstants::PHID_TYPE_CART); } - public function getTotalInCents() { - return 123; - } - public function attachPurchases(array $purchases) { assert_instances_of($purchases, 'PhortunePurchase'); $this->purchases = $purchases; diff --git a/src/applications/phortune/storage/PhortuneCharge.php b/src/applications/phortune/storage/PhortuneCharge.php index 64e3833db9..c65e89aef2 100644 --- a/src/applications/phortune/storage/PhortuneCharge.php +++ b/src/applications/phortune/storage/PhortuneCharge.php @@ -18,6 +18,7 @@ final class PhortuneCharge extends PhortuneDAO protected $accountPHID; protected $authorPHID; protected $cartPHID; + protected $paymentProviderKey; protected $paymentMethodPHID; protected $amountInCents; protected $status;