@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 Charges

Summary: Ref T2787. Makes charges a real object, allows providers to apply them. We are now (just barely) capable of stealing users' money.

Test Plan: {F179584}

Reviewers: btrahan, chad

Reviewed By: chad

Subscribers: epriestley

Maniphest Tasks: T2787

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

+322 -23
+16
resources/sql/autopatches/20140721.phortune.3.charge.sql
··· 1 + CREATE TABLE {$NAMESPACE}_phortune.phortune_charge ( 2 + id INT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY, 3 + phid VARCHAR(64) NOT NULL COLLATE utf8_bin, 4 + accountPHID VARCHAR(64) NOT NULL COLLATE utf8_bin, 5 + authorPHID VARCHAR(64) NOT NULL COLLATE utf8_bin, 6 + cartPHID VARCHAR(64) NOT NULL COLLATE utf8_bin, 7 + paymentMethodPHID VARCHAR(64) NOT NULL COLLATE utf8_bin, 8 + amountInCents INT NOT NULL, 9 + status VARCHAR(32) NOT NULL COLLATE utf8_bin, 10 + metadata LONGTEXT NOT NULL COLLATE utf8_bin, 11 + dateCreated INT UNSIGNED NOT NULL, 12 + dateModified INT UNSIGNED NOT NULL, 13 + UNIQUE KEY `key_phid` (phid), 14 + KEY `key_cart` (cartPHID), 15 + KEY `key_account` (accountPHID) 16 + ) ENGINE=InnoDB, COLLATE utf8_general_ci;
+7 -1
src/__phutil_library_map__.php
··· 2498 2498 'PhortuneCart' => 'applications/phortune/storage/PhortuneCart.php', 2499 2499 'PhortuneCartQuery' => 'applications/phortune/query/PhortuneCartQuery.php', 2500 2500 'PhortuneCharge' => 'applications/phortune/storage/PhortuneCharge.php', 2501 + 'PhortuneChargeQuery' => 'applications/phortune/query/PhortuneChargeQuery.php', 2501 2502 'PhortuneConstants' => 'applications/phortune/constants/PhortuneConstants.php', 2502 2503 'PhortuneController' => 'applications/phortune/controller/PhortuneController.php', 2503 2504 'PhortuneCreditCardForm' => 'applications/phortune/view/PhortuneCreditCardForm.php', ··· 5382 5383 'PhabricatorPolicyInterface', 5383 5384 ), 5384 5385 'PhortuneCartQuery' => 'PhabricatorCursorPagedPolicyAwareQuery', 5385 - 'PhortuneCharge' => 'PhortuneDAO', 5386 + 'PhortuneCharge' => array( 5387 + 'PhortuneDAO', 5388 + 'PhabricatorPolicyInterface', 5389 + ), 5390 + 'PhortuneChargeQuery' => 'PhabricatorCursorPagedPolicyAwareQuery', 5386 5391 'PhortuneController' => 'PhabricatorController', 5392 + 'PhortuneCurrency' => 'Phobject', 5387 5393 'PhortuneCurrencyTestCase' => 'PhabricatorTestCase', 5388 5394 'PhortuneDAO' => 'PhabricatorLiskDAO', 5389 5395 'PhortuneErrCode' => 'PhortuneConstants',
+57 -14
src/applications/phortune/controller/PhortuneAccountBuyController.php
··· 11 11 12 12 public function processRequest() { 13 13 $request = $this->getRequest(); 14 - $user = $request->getUser(); 14 + $viewer = $request->getUser(); 15 15 16 16 $cart = id(new PhortuneCartQuery()) 17 - ->setViewer($user) 17 + ->setViewer($viewer) 18 18 ->withIDs(array($this->id)) 19 19 ->needPurchases(true) 20 20 ->executeOne(); ··· 25 25 $account = $cart->getAccount(); 26 26 $account_uri = $this->getApplicationURI($account->getID().'/'); 27 27 28 + $methods = id(new PhortunePaymentMethodQuery()) 29 + ->setViewer($viewer) 30 + ->withAccountPHIDs(array($account->getPHID())) 31 + ->withStatus(PhortunePaymentMethodQuery::STATUS_OPEN) 32 + ->execute(); 33 + 34 + $e_method = null; 35 + $errors = array(); 36 + 37 + if ($request->isFormPost()) { 38 + 39 + // Require CAN_EDIT on the cart to actually make purchases. 40 + 41 + PhabricatorPolicyFilter::requireCapability( 42 + $viewer, 43 + $cart, 44 + PhabricatorPolicyCapability::CAN_EDIT); 45 + 46 + $method_id = $request->getInt('paymentMethodID'); 47 + $method = idx($methods, $method_id); 48 + if (!$method) { 49 + $e_method = pht('Required'); 50 + $errors[] = pht('You must choose a payment method.'); 51 + } 52 + 53 + if (!$errors) { 54 + $provider = $method->buildPaymentProvider(); 55 + 56 + $charge = id(new PhortuneCharge()) 57 + ->setAccountPHID($account->getPHID()) 58 + ->setCartPHID($cart->getPHID()) 59 + ->setAuthorPHID($viewer->getPHID()) 60 + ->setPaymentMethodPHID($method->getPHID()) 61 + ->setAmountInCents($cart->getTotalPriceInCents()) 62 + ->setStatus(PhortuneCharge::STATUS_PENDING); 63 + 64 + $charge->openTransaction(); 65 + $charge->save(); 66 + 67 + // TODO: We should be setting some kind of status on the cart here. 68 + $cart->save(); 69 + $charge->saveTransaction(); 70 + 71 + $provider->applyCharge($method, $charge); 72 + 73 + throw new Exception('Executed a charge! Your money is gone forever!'); 74 + } 75 + } 76 + 77 + 28 78 $rows = array(); 29 79 $total = 0; 30 80 foreach ($cart->getPurchases() as $purchase) { ··· 66 116 67 117 $cart_box = id(new PHUIObjectBoxView()) 68 118 ->setHeaderText(pht('Your Cart')) 119 + ->setFormErrors($errors) 69 120 ->appendChild($table); 70 121 71 122 $title = pht('Buy Stuff'); 72 123 73 - 74 - $methods = id(new PhortunePaymentMethodQuery()) 75 - ->setViewer($user) 76 - ->withAccountPHIDs(array($account->getPHID())) 77 - ->withStatus(PhortunePaymentMethodQuery::STATUS_OPEN) 78 - ->execute(); 79 - 80 - $method_control = id(new AphrontFormRadioButtonControl()) 81 - ->setLabel(pht('Payment Method')); 82 - 83 124 if (!$methods) { 84 125 $method_control = id(new AphrontFormStaticControl()) 85 126 ->setLabel(pht('Payment Method')) ··· 98 139 } 99 140 } 100 141 142 + $method_control->setError($e_method); 143 + 101 144 $payment_method_uri = $this->getApplicationURI( 102 145 $account->getID().'/paymentmethod/edit/'); 103 146 104 147 $form = id(new AphrontFormView()) 105 - ->setUser($user) 148 + ->setUser($viewer) 106 149 ->appendChild($method_control); 107 150 108 151 $add_providers = PhortunePaymentProvider::getProvidersForAddPaymentMethod(); ··· 137 180 $one_time_options[] = $provider->renderOneTimePaymentButton( 138 181 $account, 139 182 $cart, 140 - $user); 183 + $viewer); 141 184 } 142 185 143 186 $provider_form = new PHUIFormLayoutView();
+52
src/applications/phortune/controller/PhortuneAccountViewController.php
··· 56 56 57 57 $payment_methods = $this->buildPaymentMethodsSection($account); 58 58 $purchase_history = $this->buildPurchaseHistorySection($account); 59 + $charge_history = $this->buildChargeHistorySection($account); 59 60 $account_history = $this->buildAccountHistorySection($account); 60 61 61 62 $object_box = id(new PHUIObjectBoxView()) ··· 68 69 $object_box, 69 70 $payment_methods, 70 71 $purchase_history, 72 + $charge_history, 71 73 $account_history, 72 74 ), 73 75 array( ··· 139 141 140 142 return id(new PHUIObjectBoxView()) 141 143 ->setHeader($header); 144 + } 145 + 146 + private function buildChargeHistorySection(PhortuneAccount $account) { 147 + $request = $this->getRequest(); 148 + $viewer = $request->getUser(); 149 + 150 + $charges = id(new PhortuneChargeQuery()) 151 + ->setViewer($viewer) 152 + ->withAccountPHIDs(array($account->getPHID())) 153 + ->execute(); 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); 142 194 } 143 195 144 196 private function buildAccountHistorySection(PhortuneAccount $account) {
+13 -1
src/applications/phortune/currency/PhortuneCurrency.php
··· 1 1 <?php 2 2 3 - final class PhortuneCurrency { 3 + final class PhortuneCurrency extends Phobject { 4 4 5 5 private $value; 6 6 private $currency; ··· 50 50 $obj->currency = $currency; 51 51 52 52 return $obj; 53 + } 54 + 55 + public static function newFromList(array $list) { 56 + assert_instances_of($list, 'PhortuneCurrency'); 57 + 58 + $total = 0; 59 + foreach ($list as $item) { 60 + // TODO: This should check for integer overflows, etc. 61 + $total += $item->getValue(); 62 + } 63 + 64 + return PhortuneCurrency::newFromUSDCents($total); 53 65 } 54 66 55 67 public static function newFromUSDCents($cents) {
+13
src/applications/phortune/provider/PhortunePaymentProvider.php
··· 96 96 abstract public function canHandlePaymentMethod( 97 97 PhortunePaymentMethod $method); 98 98 99 + final public function applyCharge( 100 + PhortunePaymentMethod $payment_method, 101 + PhortuneCharge $charge) { 102 + 103 + $charge->setStatus(PhortuneCharge::STATUS_CHARGING); 104 + $charge->save(); 105 + 106 + $this->executeCharge($payment_method, $charge); 107 + 108 + $charge->setStatus(PhortuneCharge::STATUS_CHARGED); 109 + $charge->save(); 110 + } 111 + 99 112 abstract protected function executeCharge( 100 113 PhortunePaymentMethod $payment_method, 101 114 PhortuneCharge $charge);
+3
src/applications/phortune/provider/PhortuneStripePaymentProvider.php
··· 40 40 PhortunePaymentMethod $method, 41 41 PhortuneCharge $charge) { 42 42 43 + $root = dirname(phutil_get_library_root('phabricator')); 44 + require_once $root.'/externals/stripe-php/lib/Stripe.php'; 45 + 43 46 $secret_key = $this->getSecretKey(); 44 47 $params = array( 45 48 'amount' => $charge->getAmountInCents(),
+1
src/applications/phortune/query/PhortuneCartQuery.php
··· 49 49 $account = idx($accounts, $cart->getAccountPHID()); 50 50 if (!$account) { 51 51 unset($carts[$key]); 52 + continue; 52 53 } 53 54 $cart->attachAccount($account); 54 55 }
+93
src/applications/phortune/query/PhortuneChargeQuery.php
··· 1 + <?php 2 + 3 + final class PhortuneChargeQuery 4 + extends PhabricatorCursorPagedPolicyAwareQuery { 5 + 6 + private $ids; 7 + private $phids; 8 + private $accountPHIDs; 9 + 10 + public function withIDs(array $ids) { 11 + $this->ids = $ids; 12 + return $this; 13 + } 14 + 15 + public function withPHIDs(array $phids) { 16 + $this->phids = $phids; 17 + return $this; 18 + } 19 + 20 + public function withAccountPHIDs(array $account_phids) { 21 + $this->accountPHIDs = $account_phids; 22 + return $this; 23 + } 24 + 25 + protected function loadPage() { 26 + $table = new PhortuneCharge(); 27 + $conn = $table->establishConnection('r'); 28 + 29 + $rows = queryfx_all( 30 + $conn, 31 + 'SELECT charge.* FROM %T charge %Q %Q %Q', 32 + $table->getTableName(), 33 + $this->buildWhereClause($conn), 34 + $this->buildOrderClause($conn), 35 + $this->buildLimitClause($conn)); 36 + 37 + return $table->loadAllFromArray($rows); 38 + } 39 + 40 + protected function willFilterPage(array $charges) { 41 + $accounts = id(new PhortuneAccountQuery()) 42 + ->setViewer($this->getViewer()) 43 + ->setParentQuery($this) 44 + ->withPHIDs(mpull($charges, 'getAccountPHID')) 45 + ->execute(); 46 + $accounts = mpull($accounts, null, 'getPHID'); 47 + 48 + foreach ($charges as $key => $charge) { 49 + $account = idx($accounts, $charge->getAccountPHID()); 50 + if (!$account) { 51 + unset($charges[$key]); 52 + continue; 53 + } 54 + $charge->attachAccount($account); 55 + } 56 + 57 + return $charges; 58 + } 59 + 60 + private function buildWhereClause(AphrontDatabaseConnection $conn) { 61 + $where = array(); 62 + 63 + $where[] = $this->buildPagingClause($conn); 64 + 65 + if ($this->ids !== null) { 66 + $where[] = qsprintf( 67 + $conn, 68 + 'charge.id IN (%Ld)', 69 + $this->ids); 70 + } 71 + 72 + if ($this->phids !== null) { 73 + $where[] = qsprintf( 74 + $conn, 75 + 'charge.phid IN (%Ls)', 76 + $this->phids); 77 + } 78 + 79 + if ($this->accountPHIDs !== null) { 80 + $where[] = qsprintf( 81 + $conn, 82 + 'charge.accountPHID IN (%Ls)', 83 + $this->accountPHIDs); 84 + } 85 + 86 + return $this->formatWhereClause($where); 87 + } 88 + 89 + public function getQueryApplicationClass() { 90 + return 'PhabricatorApplicationPhortune'; 91 + } 92 + 93 + }
+3 -1
src/applications/phortune/query/PhortunePurchaseQuery.php
··· 38 38 } 39 39 40 40 protected function willFilterPage(array $purchases) { 41 - $carts = id(new PhabricatorObjectQuery()) 41 + $carts = id(new PhortuneCartQuery()) 42 42 ->setViewer($this->getViewer()) 43 43 ->setParentQuery($this) 44 44 ->withPHIDs(mpull($purchases, 'getCartPHID')) 45 45 ->execute(); 46 + $carts = mpull($carts, null, 'getPHID'); 46 47 47 48 foreach ($purchases as $key => $purchase) { 48 49 $cart = idx($carts, $purchase->getCartPHID()); 49 50 if (!$cart) { 50 51 unset($purchases[$key]); 52 + continue; 51 53 } 52 54 $purchase->attachCart($cart); 53 55 }
+10
src/applications/phortune/storage/PhortuneCart.php
··· 47 47 return $this->assertAttached($this->account); 48 48 } 49 49 50 + public function getTotalPriceInCents() { 51 + $prices = array(); 52 + foreach ($this->getPurchases() as $purchase) { 53 + $prices[] = PhortuneCurrency::newFromUSDCents( 54 + $purchase->getTotalPriceInCents()); 55 + } 56 + 57 + return PhortuneCurrency::newFromList($prices)->getValue(); 58 + } 59 + 50 60 51 61 /* -( PhabricatorPolicyInterface )----------------------------------------- */ 52 62
+54 -6
src/applications/phortune/storage/PhortuneCharge.php
··· 2 2 3 3 /** 4 4 * A charge is a charge (or credit) against an account and represents an actual 5 - * transfer of funds. Each charge is normally associated with a product, but a 6 - * product may have multiple charges. For example, a subscription may have 7 - * monthly charges, or a product may have a failed charge followed by a 8 - * successful charge. 5 + * transfer of funds. Each charge is normally associated with a cart, but a 6 + * cart may have multiple charges. For example, a product may have a failed 7 + * charge followed by a successful charge. 9 8 */ 10 - final class PhortuneCharge extends PhortuneDAO { 9 + final class PhortuneCharge extends PhortuneDAO 10 + implements PhabricatorPolicyInterface { 11 11 12 12 const STATUS_PENDING = 'charge:pending'; 13 13 const STATUS_AUTHORIZED = 'charge:authorized'; ··· 16 16 const STATUS_FAILED = 'charge:failed'; 17 17 18 18 protected $accountPHID; 19 - protected $purchasePHID; 19 + protected $authorPHID; 20 + protected $cartPHID; 20 21 protected $paymentMethodPHID; 21 22 protected $amountInCents; 22 23 protected $status; 23 24 protected $metadata = array(); 25 + 26 + private $account = self::ATTACHABLE; 24 27 25 28 public function getConfiguration() { 26 29 return array( ··· 34 37 public function generatePHID() { 35 38 return PhabricatorPHID::generateNewPHID( 36 39 PhabricatorPHIDConstants::PHID_TYPE_CHRG); 40 + } 41 + 42 + protected function didReadData() { 43 + // The payment processing code is strict about types. 44 + $this->amountInCents = (int)$this->amountInCents; 45 + } 46 + 47 + public function getMetadataValue($key, $default = null) { 48 + return idx($this->metadata, $key, $default); 49 + } 50 + 51 + public function setMetadataValue($key, $value) { 52 + $this->metadata[$key] = $value; 53 + return $this; 54 + } 55 + 56 + public function getAccount() { 57 + return $this->assertAttached($this->account); 58 + } 59 + 60 + public function attachAccount(PhortuneAccount $account) { 61 + $this->account = $account; 62 + return $this; 63 + } 64 + 65 + 66 + /* -( PhabricatorPolicyInterface )----------------------------------------- */ 67 + 68 + 69 + public function getCapabilities() { 70 + return array( 71 + PhabricatorPolicyCapability::CAN_VIEW, 72 + ); 73 + } 74 + 75 + public function getPolicy($capability) { 76 + return $this->getAccount()->getPolicy($capability); 77 + } 78 + 79 + public function hasAutomaticCapability($capability, PhabricatorUser $viewer) { 80 + return $this->getAccount()->hasAutomaticCapability($capability, $viewer); 81 + } 82 + 83 + public function describeAutomaticCapability($capability) { 84 + return pht('Charges inherit the policies of the associated account.'); 37 85 } 38 86 39 87 }