@recaptime-dev's working patches + fork for Phorge, a community fork of Phabricator. (Upstream dev and stable branches are at upstream/main and upstream/stable respectively.) hq.recaptime.dev/wiki/Phorge
phorge phabricator
1
fork

Configure Feed

Select the types of activity you want to include in your feed.

Phortune Cart Status, some one-time support

Summary:
Ref T2787. Carts need a status so we can tell if they've been purchased. Also kind of get WePay working as a one-time provider, and let charges not have a methodPHID (they won't for one-time providers).

All the status stuff is still super crazy rough and you can do things like start a checkout, add a bunch of stuff to your cart, complete the checkout, and have Phabricator think you paid for all the stuff you added. But this is fine for now since you can't actually edit carts, and also none of this is at all usable anyway. I'll refine some of the workflows in future diffs, for now I'm just getting things hooked up and technically working.

Test Plan:
- Purcahsed a cart and got a sort of status/done screen instead of a "your money is gone" exception.
- Went through the WePay flow and got a successful test checkout.

Reviewers: btrahan, chad

Reviewed By: chad

Subscribers: epriestley

Maniphest Tasks: T2787

Differential Revision: https://secure.phabricator.com/D10003

+273 -104
+2
resources/sql/autopatches/20140721.phortune.4.cartstatus.sql
··· 1 + ALTER TABLE {$NAMESPACE}_phortune.phortune_cart 2 + ADD status VARCHAR(32) NOT NULL COLLATE utf8_bin;
+2
resources/sql/autopatches/20140721.phortune.5.cstatusdefault.sql
··· 1 + UPDATE {$NAMESPACE}_phortune.phortune_cart 2 + SET status = 'cart:ready' WHERE status = '';
+3
resources/sql/autopatches/20140721.phortune.6.onetimecharge.sql
··· 1 + ALTER TABLE {$NAMESPACE}_phortune.phortune_charge 2 + ADD paymentProviderKey VARCHAR(128) NOT NULL COLLATE utf8_bin 3 + AFTER cartPHID;
+4
resources/sql/autopatches/20140721.phortune.7.nullmethod.sql
··· 1 + /* Make this nullable to support one-time providers. */ 2 + 3 + ALTER TABLE {$NAMESPACE}_phortune.phortune_charge 4 + CHANGE paymentMethodPHID paymentMethodPHID VARCHAR(64) COLLATE utf8_bin;
+6 -2
src/__phutil_library_map__.php
··· 2488 2488 'PholioTransactionView' => 'applications/pholio/view/PholioTransactionView.php', 2489 2489 'PholioUploadedImageView' => 'applications/pholio/view/PholioUploadedImageView.php', 2490 2490 'PhortuneAccount' => 'applications/phortune/storage/PhortuneAccount.php', 2491 - 'PhortuneAccountBuyController' => 'applications/phortune/controller/PhortuneAccountBuyController.php', 2492 2491 'PhortuneAccountEditor' => 'applications/phortune/editor/PhortuneAccountEditor.php', 2493 2492 'PhortuneAccountQuery' => 'applications/phortune/query/PhortuneAccountQuery.php', 2494 2493 'PhortuneAccountTransaction' => 'applications/phortune/storage/PhortuneAccountTransaction.php', ··· 2496 2495 'PhortuneAccountViewController' => 'applications/phortune/controller/PhortuneAccountViewController.php', 2497 2496 'PhortuneBalancedPaymentProvider' => 'applications/phortune/provider/PhortuneBalancedPaymentProvider.php', 2498 2497 'PhortuneCart' => 'applications/phortune/storage/PhortuneCart.php', 2498 + 'PhortuneCartCheckoutController' => 'applications/phortune/controller/PhortuneCartCheckoutController.php', 2499 + 'PhortuneCartController' => 'applications/phortune/controller/PhortuneCartController.php', 2499 2500 'PhortuneCartQuery' => 'applications/phortune/query/PhortuneCartQuery.php', 2501 + 'PhortuneCartViewController' => 'applications/phortune/controller/PhortuneCartViewController.php', 2500 2502 'PhortuneCharge' => 'applications/phortune/storage/PhortuneCharge.php', 2501 2503 'PhortuneChargeQuery' => 'applications/phortune/query/PhortuneChargeQuery.php', 2502 2504 'PhortuneConstants' => 'applications/phortune/constants/PhortuneConstants.php', ··· 5371 5373 'PhortuneDAO', 5372 5374 'PhabricatorPolicyInterface', 5373 5375 ), 5374 - 'PhortuneAccountBuyController' => 'PhortuneController', 5375 5376 'PhortuneAccountEditor' => 'PhabricatorApplicationTransactionEditor', 5376 5377 'PhortuneAccountQuery' => 'PhabricatorCursorPagedPolicyAwareQuery', 5377 5378 'PhortuneAccountTransaction' => 'PhabricatorApplicationTransaction', ··· 5382 5383 'PhortuneDAO', 5383 5384 'PhabricatorPolicyInterface', 5384 5385 ), 5386 + 'PhortuneCartCheckoutController' => 'PhortuneCartController', 5387 + 'PhortuneCartController' => 'PhortuneController', 5385 5388 'PhortuneCartQuery' => 'PhabricatorCursorPagedPolicyAwareQuery', 5389 + 'PhortuneCartViewController' => 'PhortuneCartController', 5386 5390 'PhortuneCharge' => array( 5387 5391 'PhortuneDAO', 5388 5392 'PhabricatorPolicyInterface',
+4 -1
src/applications/phortune/application/PhabricatorPhortuneApplication.php
··· 41 41 ), 42 42 'buy/(?P<productID>\d+)/' => 'PhortuneProductPurchaseController', 43 43 ), 44 - 'cart/(?P<id>\d+)/' => 'PhortuneAccountBuyController', 44 + 'cart/(?P<id>\d+)/' => array( 45 + '' => 'PhortuneCartViewController', 46 + 'checkout/' => 'PhortuneCartCheckoutController', 47 + ), 45 48 'account/' => array( 46 49 '' => 'PhortuneAccountListController', 47 50 'edit/(?:(?P<id>\d+)/)?' => 'PhortuneAccountEditController',
+10 -47
src/applications/phortune/controller/PhortuneAccountBuyController.php src/applications/phortune/controller/PhortuneCartCheckoutController.php
··· 1 1 <?php 2 2 3 - final class PhortuneAccountBuyController 4 - extends PhortuneController { 3 + final class PhortuneCartCheckoutController 4 + extends PhortuneCartController { 5 5 6 6 private $id; 7 7 ··· 64 64 $charge->openTransaction(); 65 65 $charge->save(); 66 66 67 - // TODO: We should be setting some kind of status on the cart here. 67 + $cart->setStatus(PhortuneCart::STATUS_PURCHASING); 68 68 $cart->save(); 69 69 $charge->saveTransaction(); 70 70 71 71 $provider->applyCharge($method, $charge); 72 72 73 - throw new Exception('Executed a charge! Your money is gone forever!'); 74 - } 75 - } 73 + $cart->setStatus(PhortuneCart::STATUS_PURCHASED); 74 + $cart->save(); 76 75 76 + $view_uri = $this->getApplicationURI('cart/'.$cart->getID().'/'); 77 77 78 - $rows = array(); 79 - $total = 0; 80 - foreach ($cart->getPurchases() as $purchase) { 81 - $rows[] = array( 82 - pht('A Purchase'), 83 - PhortuneCurrency::newFromUSDCents($purchase->getBasePriceInCents()) 84 - ->formatForDisplay(), 85 - $purchase->getQuantity(), 86 - PhortuneCurrency::newFromUSDCents($purchase->getTotalPriceInCents()) 87 - ->formatForDisplay(), 88 - ); 89 - 90 - $total += $purchase->getTotalPriceInCents(); 78 + return id(new AphrontRedirectResponse())->setURI($view_uri); 79 + } 91 80 } 92 81 93 - $rows[] = array( 94 - phutil_tag('strong', array(), pht('Total')), 95 - '', 96 - '', 97 - phutil_tag('strong', array(), 98 - PhortuneCurrency::newFromUSDCents($total)->formatForDisplay()), 99 - ); 100 - 101 - $table = new AphrontTableView($rows); 102 - $table->setHeaders( 103 - array( 104 - pht('Item'), 105 - pht('Price'), 106 - pht('Qty.'), 107 - pht('Total'), 108 - )); 109 - $table->setColumnClasses( 110 - array( 111 - 'wide', 112 - 'right', 113 - 'right', 114 - 'right', 115 - )); 116 - 117 - $cart_box = id(new PHUIObjectBoxView()) 118 - ->setHeaderText(pht('Your Cart')) 119 - ->setFormErrors($errors) 120 - ->appendChild($table); 82 + $cart_box = $this->buildCartContents($cart); 83 + $cart_box->setFormErrors($errors); 121 84 122 85 $title = pht('Buy Stuff'); 123 86
+1 -39
src/applications/phortune/controller/PhortuneAccountViewController.php
··· 152 152 ->withAccountPHIDs(array($account->getPHID())) 153 153 ->execute(); 154 154 155 - $rows = array(); 156 - foreach ($charges as $charge) { 157 - $rows[] = array( 158 - $charge->getID(), 159 - $charge->getCartPHID(), 160 - $charge->getPaymentMethodPHID(), 161 - PhortuneCurrency::newFromUSDCents($charge->getAmountInCents()) 162 - ->formatForDisplay(), 163 - $charge->getStatus(), 164 - phabricator_datetime($charge->getDateCreated(), $viewer), 165 - ); 166 - } 167 - 168 - $charge_table = id(new AphrontTableView($rows)) 169 - ->setHeaders( 170 - array( 171 - pht('Charge ID'), 172 - pht('Cart'), 173 - pht('Method'), 174 - pht('Amount'), 175 - pht('Status'), 176 - pht('Created'), 177 - )) 178 - ->setColumnClasses( 179 - array( 180 - '', 181 - '', 182 - '', 183 - 'wide right', 184 - '', 185 - '', 186 - )); 187 - 188 - $header = id(new PHUIHeaderView()) 189 - ->setHeader(pht('Charge History')); 190 - 191 - return id(new PHUIObjectBoxView()) 192 - ->setHeader($header) 193 - ->appendChild($charge_table); 155 + return $this->buildChargesTable($charges); 194 156 } 195 157 196 158 private function buildAccountHistorySection(PhortuneAccount $account) {
+52
src/applications/phortune/controller/PhortuneCartController.php
··· 1 + <?php 2 + 3 + abstract class PhortuneCartController 4 + extends PhortuneController { 5 + 6 + protected function buildCartContents(PhortuneCart $cart) { 7 + 8 + $rows = array(); 9 + $total = 0; 10 + foreach ($cart->getPurchases() as $purchase) { 11 + $rows[] = array( 12 + pht('A Purchase'), 13 + PhortuneCurrency::newFromUSDCents($purchase->getBasePriceInCents()) 14 + ->formatForDisplay(), 15 + $purchase->getQuantity(), 16 + PhortuneCurrency::newFromUSDCents($purchase->getTotalPriceInCents()) 17 + ->formatForDisplay(), 18 + ); 19 + 20 + $total += $purchase->getTotalPriceInCents(); 21 + } 22 + 23 + $rows[] = array( 24 + phutil_tag('strong', array(), pht('Total')), 25 + '', 26 + '', 27 + phutil_tag('strong', array(), 28 + PhortuneCurrency::newFromUSDCents($total)->formatForDisplay()), 29 + ); 30 + 31 + $table = new AphrontTableView($rows); 32 + $table->setHeaders( 33 + array( 34 + pht('Item'), 35 + pht('Price'), 36 + pht('Qty.'), 37 + pht('Total'), 38 + )); 39 + $table->setColumnClasses( 40 + array( 41 + 'wide', 42 + 'right', 43 + 'right', 44 + 'right', 45 + )); 46 + 47 + return id(new PHUIObjectBoxView()) 48 + ->setHeaderText(pht('Cart Contents')) 49 + ->appendChild($table); 50 + } 51 + 52 + }
+50
src/applications/phortune/controller/PhortuneCartViewController.php
··· 1 + <?php 2 + 3 + final class PhortuneCartViewController 4 + extends PhortuneCartController { 5 + 6 + private $id; 7 + 8 + public function willProcessRequest(array $data) { 9 + $this->id = $data['id']; 10 + } 11 + 12 + public function processRequest() { 13 + $request = $this->getRequest(); 14 + $viewer = $request->getUser(); 15 + 16 + $cart = id(new PhortuneCartQuery()) 17 + ->setViewer($viewer) 18 + ->withIDs(array($this->id)) 19 + ->needPurchases(true) 20 + ->executeOne(); 21 + if (!$cart) { 22 + return new Aphront404Response(); 23 + } 24 + 25 + $cart_box = $this->buildCartContents($cart); 26 + 27 + $charges = id(new PhortuneChargeQuery()) 28 + ->setViewer($viewer) 29 + ->withCartPHIDs(array($cart->getPHID())) 30 + ->execute(); 31 + 32 + $charges_table = $this->buildChargesTable($charges); 33 + 34 + $account = $cart->getAccount(); 35 + 36 + $crumbs = $this->buildApplicationCrumbs(); 37 + $crumbs->addTextCrumb(pht('Cart')); 38 + 39 + return $this->buildApplicationPage( 40 + array( 41 + $crumbs, 42 + $cart_box, 43 + $charges_table, 44 + ), 45 + array( 46 + 'title' => pht('Cart'), 47 + )); 48 + 49 + } 50 + }
+47
src/applications/phortune/controller/PhortuneController.php
··· 52 52 return $account; 53 53 } 54 54 55 + protected function buildChargesTable(array $charges) { 56 + $request = $this->getRequest(); 57 + $viewer = $request->getUser(); 58 + 59 + $rows = array(); 60 + foreach ($charges as $charge) { 61 + $rows[] = array( 62 + $charge->getID(), 63 + $charge->getCartPHID(), 64 + $charge->getPaymentProviderKey(), 65 + $charge->getPaymentMethodPHID(), 66 + PhortuneCurrency::newFromUSDCents($charge->getAmountInCents()) 67 + ->formatForDisplay(), 68 + $charge->getStatus(), 69 + phabricator_datetime($charge->getDateCreated(), $viewer), 70 + ); 71 + } 72 + 73 + $charge_table = id(new AphrontTableView($rows)) 74 + ->setHeaders( 75 + array( 76 + pht('Charge ID'), 77 + pht('Cart'), 78 + pht('Provider'), 79 + pht('Method'), 80 + pht('Amount'), 81 + pht('Status'), 82 + pht('Created'), 83 + )) 84 + ->setColumnClasses( 85 + array( 86 + '', 87 + '', 88 + '', 89 + '', 90 + 'wide right', 91 + '', 92 + '', 93 + )); 94 + 95 + $header = id(new PHUIHeaderView()) 96 + ->setHeader(pht('Charge History')); 97 + 98 + return id(new PHUIObjectBoxView()) 99 + ->setHeader($header) 100 + ->appendChild($charge_table); 101 + } 55 102 56 103 }
+3 -1
src/applications/phortune/controller/PhortuneProductPurchaseController.php
··· 39 39 $cart = new PhortuneCart(); 40 40 $cart->openTransaction(); 41 41 42 + $cart->setStatus(PhortuneCart::STATUS_READY); 42 43 $cart->setAccountPHID($account->getPHID()); 43 44 $cart->setAuthorPHID($user->getPHID()); 44 45 $cart->save(); ··· 57 58 58 59 $cart->saveTransaction(); 59 60 60 - $cart_uri = $this->getApplicationURI('/cart/'.$cart->getID().'/'); 61 + $cart_id = $cart->getID(); 62 + $cart_uri = $this->getApplicationURI('/cart/'.$cart_id.'/checkout/'); 61 63 return id(new AphrontRedirectResponse())->setURI($cart_uri); 62 64 } 63 65
+13 -1
src/applications/phortune/controller/PhortuneProviderController.php
··· 56 56 57 57 58 58 public function loadCart($id) { 59 - return id(new PhortuneCart()); 59 + $request = $this->getRequest(); 60 + $viewer = $request->getUser(); 61 + 62 + return id(new PhortuneCartQuery()) 63 + ->setViewer($viewer) 64 + ->needPurchases(true) 65 + ->withIDs(array($id)) 66 + ->requireCapabilities( 67 + array( 68 + PhabricatorPolicyCapability::CAN_VIEW, 69 + PhabricatorPolicyCapability::CAN_EDIT, 70 + )) 71 + ->executeOne(); 60 72 } 61 73 62 74 }
+1 -1
src/applications/phortune/provider/PhortunePaypalPaymentProvider.php
··· 121 121 'cartID' => $cart->getID(), 122 122 )); 123 123 124 - $total_in_cents = $cart->getTotalInCents(); 124 + $total_in_cents = $cart->getTotalPriceInCents(); 125 125 $price = PhortuneCurrency::newFromUSDCents($total_in_cents); 126 126 127 127 $result = $this
+56 -8
src/applications/phortune/provider/PhortuneWePayPaymentProvider.php
··· 111 111 PhortuneProviderController $controller, 112 112 AphrontRequest $request) { 113 113 114 + $viewer = $request->getUser(); 115 + 114 116 $cart = $controller->loadCart($request->getInt('cartID')); 115 117 if (!$cart) { 116 118 return new Aphront404Response(); 117 119 } 120 + 121 + $cart_uri = '/phortune/cart/'.$cart->getID().'/'; 118 122 119 123 $root = dirname(phutil_get_library_root('phabricator')); 120 124 require_once $root.'/externals/wepay/wepay.php'; ··· 139 143 'cartID' => $cart->getID(), 140 144 )); 141 145 142 - $total_in_cents = $cart->getTotalInCents(); 146 + $total_in_cents = $cart->getTotalPriceInCents(); 143 147 $price = PhortuneCurrency::newFromUSDCents($total_in_cents); 144 148 145 149 $params = array( ··· 153 157 'fee_payer' => 'Payee', 154 158 'redirect_uri' => $return_uri, 155 159 'fallback_uri' => $cancel_uri, 156 - 'auto_capture' => false, 160 + 161 + // NOTE: If we don't `auto_capture`, we might get a result back in 162 + // either an "authorized" or a "reserved" state. We can't capture 163 + // an "authorized" result, so just autocapture. 164 + 165 + 'auto_capture' => true, 157 166 'require_shipping' => 0, 158 167 'shipping_fee' => 0, 159 168 'charge_tax' => 0, ··· 163 172 164 173 $result = $wepay->request('checkout/create', $params); 165 174 166 - // NOTE: We might want to store "$result->checkout_id" on the Cart. 175 + // TODO: We must store "$result->checkout_id" on the Cart since the 176 + // user might not end up back here. Really this needs a bunch of junk. 167 177 168 178 $uri = new PhutilURI($result->checkout_uri); 169 179 return id(new AphrontRedirectResponse())->setURI($uri); 170 180 case 'charge': 181 + $checkout_id = $request->getInt('checkout_id'); 182 + $params = array( 183 + 'checkout_id' => $checkout_id, 184 + ); 171 185 172 - // NOTE: We get $_REQUEST['checkout_id'] here, but our parameters are 173 - // dropped so we should stop depending on them or shove them into the 174 - // URI. 186 + $checkout = $wepay->request('checkout', $params); 187 + if ($checkout->reference_id != $cart->getPHID()) { 188 + throw new Exception( 189 + pht('Checkout reference ID does not match cart PHID!')); 190 + } 175 191 176 - var_dump($_REQUEST); 177 - break; 192 + switch ($checkout->state) { 193 + case 'authorized': 194 + case 'reserved': 195 + case 'captured': 196 + break; 197 + default: 198 + throw new Exception( 199 + pht( 200 + 'Checkout is in bad state "%s"!', 201 + $result->state)); 202 + } 203 + 204 + $unguarded = AphrontWriteGuard::beginScopedUnguardedWrites(); 205 + 206 + $charge = id(new PhortuneCharge()) 207 + ->setAmountInCents((int)$checkout->gross * 100) 208 + ->setAccountPHID($cart->getAccount()->getPHID()) 209 + ->setAuthorPHID($viewer->getPHID()) 210 + ->setPaymentProviderKey($this->getProviderKey()) 211 + ->setCartPHID($cart->getPHID()) 212 + ->setStatus(PhortuneCharge::STATUS_CHARGING) 213 + ->save(); 214 + 215 + $cart->openTransaction(); 216 + $charge->setStatus(PhortuneCharge::STATUS_CHARGED); 217 + $charge->save(); 218 + 219 + $cart->setStatus(PhortuneCart::STATUS_PURCHASED); 220 + $cart->save(); 221 + $cart->saveTransaction(); 222 + 223 + unset($unguarded); 224 + 225 + return id(new AphrontRedirectResponse())->setURI($cart_uri); 178 226 case 'cancel': 179 227 var_dump($_REQUEST); 180 228 break;
+13
src/applications/phortune/query/PhortuneChargeQuery.php
··· 6 6 private $ids; 7 7 private $phids; 8 8 private $accountPHIDs; 9 + private $cartPHIDs; 9 10 10 11 public function withIDs(array $ids) { 11 12 $this->ids = $ids; ··· 19 20 20 21 public function withAccountPHIDs(array $account_phids) { 21 22 $this->accountPHIDs = $account_phids; 23 + return $this; 24 + } 25 + 26 + public function withCartPHIDs(array $cart_phids) { 27 + $this->cartPHIDs = $cart_phids; 22 28 return $this; 23 29 } 24 30 ··· 81 87 $conn, 82 88 'charge.accountPHID IN (%Ls)', 83 89 $this->accountPHIDs); 90 + } 91 + 92 + if ($this->cartPHIDs !== null) { 93 + $where[] = qsprintf( 94 + $conn, 95 + 'charge.cartPHID IN (%Ls)', 96 + $this->cartPHIDs); 84 97 } 85 98 86 99 return $this->formatWhereClause($where);
+5 -4
src/applications/phortune/storage/PhortuneCart.php
··· 3 3 final class PhortuneCart extends PhortuneDAO 4 4 implements PhabricatorPolicyInterface { 5 5 6 + const STATUS_READY = 'cart:ready'; 7 + const STATUS_PURCHASING = 'cart:purchasing'; 8 + const STATUS_PURCHASED = 'cart:purchased'; 9 + 6 10 protected $accountPHID; 7 11 protected $authorPHID; 12 + protected $status; 8 13 protected $metadata; 9 14 10 15 private $account = self::ATTACHABLE; ··· 22 27 public function generatePHID() { 23 28 return PhabricatorPHID::generateNewPHID( 24 29 PhabricatorPHIDConstants::PHID_TYPE_CART); 25 - } 26 - 27 - public function getTotalInCents() { 28 - return 123; 29 30 } 30 31 31 32 public function attachPurchases(array $purchases) {
+1
src/applications/phortune/storage/PhortuneCharge.php
··· 18 18 protected $accountPHID; 19 19 protected $authorPHID; 20 20 protected $cartPHID; 21 + protected $paymentProviderKey; 21 22 protected $paymentMethodPHID; 22 23 protected $amountInCents; 23 24 protected $status;