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

Implement Phortune charge updates

Summary: Ref T2787. These don't necessarily do a ton yet, but we can get PayPal out of hold, at least.

Test Plan: Updated charges from all providers. Cleared a PayPal hold.

Reviewers: btrahan

Reviewed By: btrahan

Subscribers: epriestley

Maniphest Tasks: T2787

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

+214 -68
+2
src/applications/fund/phortune/FundBackerProduct.php
··· 79 79 public function didPurchaseProduct( 80 80 PhortuneProduct $product, 81 81 PhortunePurchase $purchase) { 82 + // TODO: This viewer may be wrong if the purchase completes after a hold 83 + // we should load the backer explicitly. 82 84 $viewer = $this->getViewer(); 83 85 84 86 $backer = id(new FundBackerQuery())
+11 -2
src/applications/phortune/controller/PhortuneAccountViewController.php
··· 169 169 ->withStatuses( 170 170 array( 171 171 PhortuneCart::STATUS_PURCHASING, 172 + PhortuneCart::STATUS_CHARGED, 173 + PhortuneCart::STATUS_HOLD, 172 174 PhortuneCart::STATUS_PURCHASED, 173 175 )) 174 176 ->execute(); ··· 197 199 198 200 $rowc[] = ''; 199 201 $rows[] = array( 202 + $cart->getID(), 200 203 phutil_tag( 201 204 'strong', 202 205 array(), ··· 206 209 'strong', 207 210 array(), 208 211 $cart->getTotalPriceAsCurrency()->formatForDisplay()), 212 + PhortuneCart::getNameForStatus($cart->getStatus()), 209 213 phabricator_datetime($cart->getDateModified(), $viewer), 210 214 ); 211 215 foreach ($purchases as $purchase) { ··· 219 223 $handles[$purchase->getPHID()]->renderLink(), 220 224 $price, 221 225 '', 226 + '', 222 227 ); 223 228 } 224 229 } ··· 227 232 ->setRowClasses($rowc) 228 233 ->setHeaders( 229 234 array( 230 - pht('Cart'), 235 + pht('ID'), 236 + pht('Order'), 231 237 pht('Purchase'), 232 238 pht('Amount'), 239 + pht('Status'), 233 240 pht('Updated'), 234 241 )) 235 242 ->setColumnClasses( 236 243 array( 244 + '', 237 245 '', 238 246 'wide', 239 247 'right', 248 + '', 240 249 'right', 241 250 )); 242 251 243 252 $header = id(new PHUIHeaderView()) 244 - ->setHeader(pht('Purchase History')); 253 + ->setHeader(pht('Order History')); 245 254 246 255 return id(new PHUIObjectBoxView()) 247 256 ->setHeader($header)
+35 -1
src/applications/phortune/controller/PhortuneCartUpdateController.php
··· 22 22 return new Aphront404Response(); 23 23 } 24 24 25 - // TODO: This obviously doesn't do anything for now. 25 + $charges = id(new PhortuneChargeQuery()) 26 + ->setViewer($viewer) 27 + ->withCartPHIDs(array($cart->getPHID())) 28 + ->needCarts(true) 29 + ->withStatuses( 30 + array( 31 + PhortuneCharge::STATUS_HOLD, 32 + PhortuneCharge::STATUS_CHARGED, 33 + )) 34 + ->execute(); 35 + 36 + if ($charges) { 37 + $providers = id(new PhortunePaymentProviderConfigQuery()) 38 + ->setViewer($viewer) 39 + ->withPHIDs(mpull($charges, 'getProviderPHID')) 40 + ->execute(); 41 + $providers = mpull($providers, null, 'getPHID'); 42 + } else { 43 + $providers = array(); 44 + } 45 + 46 + foreach ($charges as $charge) { 47 + if ($charge->isRefund()) { 48 + // Don't update refunds. 49 + continue; 50 + } 51 + 52 + $provider_config = idx($providers, $charge->getProviderPHID()); 53 + if (!$provider_config) { 54 + throw new Exception(pht('Unable to load provider for charge!')); 55 + } 56 + 57 + $provider = $provider_config->buildProvider(); 58 + $provider->updateCharge($charge); 59 + } 26 60 27 61 return id(new AphrontRedirectResponse()) 28 62 ->setURI($cart->getDetailURI());
+1 -2
src/applications/phortune/controller/PhortuneCartViewController.php
··· 83 83 84 84 $header = id(new PHUIHeaderView()) 85 85 ->setUser($viewer) 86 - ->setHeader(pht('Order Detail')) 87 - ->setPolicyObject($cart); 86 + ->setHeader(pht('Order Detail')); 88 87 89 88 $cart_box = id(new PHUIObjectBoxView()) 90 89 ->setHeader($header)
+29 -19
src/applications/phortune/provider/PhortuneBalancedPaymentProvider.php
··· 102 102 } 103 103 104 104 public function runConfigurationTest() { 105 - $root = dirname(phutil_get_library_root('phabricator')); 106 - require_once $root.'/externals/httpful/bootstrap.php'; 107 - require_once $root.'/externals/restful/bootstrap.php'; 108 - require_once $root.'/externals/balanced-php/bootstrap.php'; 105 + $this->loadBalancedAPILibraries(); 109 106 110 107 // TODO: This only tests that the secret key is correct. It's not clear 111 108 // how to test that the marketplace is correct. ··· 140 137 protected function executeCharge( 141 138 PhortunePaymentMethod $method, 142 139 PhortuneCharge $charge) { 143 - 144 - $root = dirname(phutil_get_library_root('phabricator')); 145 - require_once $root.'/externals/httpful/bootstrap.php'; 146 - require_once $root.'/externals/restful/bootstrap.php'; 147 - require_once $root.'/externals/balanced-php/bootstrap.php'; 140 + $this->loadBalancedAPILibraries(); 148 141 149 142 $price = $charge->getAmountAsCurrency(); 150 143 ··· 182 175 protected function executeRefund( 183 176 PhortuneCharge $charge, 184 177 PhortuneCharge $refund) { 185 - 186 - $root = dirname(phutil_get_library_root('phabricator')); 187 - require_once $root.'/externals/httpful/bootstrap.php'; 188 - require_once $root.'/externals/restful/bootstrap.php'; 189 - require_once $root.'/externals/balanced-php/bootstrap.php'; 178 + $this->loadBalancedAPILibraries(); 190 179 191 180 $debit_uri = $charge->getMetadataValue('balanced.debitURI'); 192 181 if (!$debit_uri) { ··· 214 203 $refund->save(); 215 204 } 216 205 206 + public function updateCharge(PhortuneCharge $charge) { 207 + $this->loadBalancedAPILibraries(); 208 + 209 + $debit_uri = $charge->getMetadataValue('balanced.debitURI'); 210 + if (!$debit_uri) { 211 + throw new Exception(pht('No Balanced debit URI!')); 212 + } 213 + 214 + try { 215 + Balanced\Settings::$api_key = $this->getSecretKey(); 216 + $balanced_debit = Balanced\Debit::get($debit_uri); 217 + } catch (RESTful\Exceptions\HTTPError $error) { 218 + throw new Exception($error->response->body->description); 219 + } 220 + 221 + // TODO: Deal with disputes / chargebacks / surprising refunds. 222 + } 223 + 217 224 private function getMarketplaceID() { 218 225 return $this 219 226 ->getProviderConfig() ··· 255 262 AphrontRequest $request, 256 263 PhortunePaymentMethod $method, 257 264 array $token) { 265 + $this->loadBalancedAPILibraries(); 258 266 259 267 $errors = array(); 260 - 261 - $root = dirname(phutil_get_library_root('phabricator')); 262 - require_once $root.'/externals/httpful/bootstrap.php'; 263 - require_once $root.'/externals/restful/bootstrap.php'; 264 - require_once $root.'/externals/balanced-php/bootstrap.php'; 265 268 266 269 $account_phid = $method->getAccountPHID(); 267 270 $author_phid = $method->getAuthorPHID(); ··· 355 358 356 359 357 360 return null; 361 + } 362 + 363 + private function loadBalancedAPILibraries() { 364 + $root = dirname(phutil_get_library_root('phabricator')); 365 + require_once $root.'/externals/httpful/bootstrap.php'; 366 + require_once $root.'/externals/restful/bootstrap.php'; 367 + require_once $root.'/externals/balanced-php/bootstrap.php'; 358 368 } 359 369 360 370 }
+57
src/applications/phortune/provider/PhortunePayPalPaymentProvider.php
··· 192 192 $result['REFUNDTRANSACTIONID']); 193 193 } 194 194 195 + public function updateCharge(PhortuneCharge $charge) { 196 + $transaction_id = $charge->getMetadataValue('paypal.transactionID'); 197 + if (!$transaction_id) { 198 + throw new Exception(pht('Charge has no transaction ID!')); 199 + } 200 + 201 + $params = array( 202 + 'TRANSACTIONID' => $transaction_id, 203 + ); 204 + 205 + $result = $this 206 + ->newPaypalAPICall() 207 + ->setRawPayPalQuery('GetTransactionDetails', $params) 208 + ->resolve(); 209 + 210 + $is_charge = false; 211 + $is_fail = false; 212 + switch ($result['PAYMENTSTATUS']) { 213 + case 'Processed': 214 + case 'Completed': 215 + case 'Completed-Funds-Held': 216 + $is_charge = true; 217 + break; 218 + case 'Partially-Refunded': 219 + case 'Refunded': 220 + case 'Reversed': 221 + case 'Canceled-Reversal': 222 + // TODO: Handle these. 223 + return; 224 + case 'In-Progress': 225 + case 'Pending': 226 + // TODO: Also handle these better? 227 + return; 228 + case 'Denied': 229 + case 'Expired': 230 + case 'Failed': 231 + case 'None': 232 + case 'Voided': 233 + default: 234 + $is_fail = true; 235 + break; 236 + } 237 + 238 + if ($charge->getStatus() == PhortuneCharge::STATUS_HOLD) { 239 + $cart = $charge->getCart(); 240 + 241 + $unguarded = AphrontWriteGuard::beginScopedUnguardedWrites(); 242 + if ($is_charge) { 243 + $cart->didApplyCharge($charge); 244 + } else if ($is_fail) { 245 + $cart->didFailCharge($charge); 246 + } 247 + unset($unguarded); 248 + } 249 + } 250 + 195 251 private function getPaypalAPIUsername() { 196 252 return $this 197 253 ->getProviderConfig() ··· 278 334 'PAYMENTREQUEST_0_CURRENCYCODE' => $price->getCurrency(), 279 335 'PAYMENTREQUEST_0_PAYMENTACTION' => 'Sale', 280 336 'PAYMENTREQUEST_0_CUSTOM' => $charge->getPHID(), 337 + 'PAYMENTREQUEST_0_DESC' => $cart->getName(), 281 338 282 339 'RETURNURL' => $return_uri, 283 340 'CANCELURL' => $cancel_uri,
+3 -1
src/applications/phortune/provider/PhortunePaymentProvider.php
··· 149 149 150 150 abstract protected function executeRefund( 151 151 PhortuneCharge $charge, 152 - PhortuneCharge $charge); 152 + PhortuneCharge $refund); 153 + 154 + abstract public function updateCharge(PhortuneCharge $charge); 153 155 154 156 155 157 /* -( Adding Payment Methods )--------------------------------------------- */
+25 -11
src/applications/phortune/provider/PhortuneStripePaymentProvider.php
··· 116 116 } 117 117 118 118 public function runConfigurationTest() { 119 - $root = dirname(phutil_get_library_root('phabricator')); 120 - require_once $root.'/externals/stripe-php/lib/Stripe.php'; 119 + $this->loadStripeAPILibraries(); 121 120 122 121 $secret_key = $this->getSecretKey(); 123 122 $account = Stripe_Account::retrieve($secret_key); ··· 131 130 protected function executeCharge( 132 131 PhortunePaymentMethod $method, 133 132 PhortuneCharge $charge) { 134 - 135 - $root = dirname(phutil_get_library_root('phabricator')); 136 - require_once $root.'/externals/stripe-php/lib/Stripe.php'; 133 + $this->loadStripeAPILibraries(); 137 134 138 135 $price = $charge->getAmountAsCurrency(); 139 136 ··· 160 157 protected function executeRefund( 161 158 PhortuneCharge $charge, 162 159 PhortuneCharge $refund) { 160 + $this->loadStripeAPILibraries(); 163 161 164 162 $charge_id = $charge->getMetadataValue('stripe.chargeID'); 165 163 if (!$charge_id) { ··· 167 165 pht('Unable to refund charge; no Stripe chargeID!')); 168 166 } 169 167 170 - $root = dirname(phutil_get_library_root('phabricator')); 171 - require_once $root.'/externals/stripe-php/lib/Stripe.php'; 172 - 173 168 $refund_cents = $refund 174 169 ->getAmountAsCurrency() 175 170 ->negate() ··· 192 187 $charge->save(); 193 188 } 194 189 190 + public function updateCharge(PhortuneCharge $charge) { 191 + $this->loadStripeAPILibraries(); 192 + 193 + $charge_id = $charge->getMetadataValue('stripe.chargeID'); 194 + if (!$charge_id) { 195 + throw new Exception( 196 + pht('Unable to update charge; no Stripe chargeID!')); 197 + } 198 + 199 + $secret_key = $this->getSecretKey(); 200 + $stripe_charge = Stripe_Charge::retrieve($charge_id, $secret_key); 201 + 202 + // TODO: Deal with disputes / chargebacks / surprising refunds. 203 + 204 + } 205 + 195 206 private function getPublishableKey() { 196 207 return $this 197 208 ->getProviderConfig() ··· 221 232 AphrontRequest $request, 222 233 PhortunePaymentMethod $method, 223 234 array $token) { 235 + $this->loadStripeAPILibraries(); 224 236 225 237 $errors = array(); 226 - 227 - $root = dirname(phutil_get_library_root('phabricator')); 228 - require_once $root.'/externals/stripe-php/lib/Stripe.php'; 229 238 230 239 $secret_key = $this->getSecretKey(); 231 240 $stripe_token = $token['stripeCardToken']; ··· 360 369 } 361 370 362 371 return null; 372 + } 373 + 374 + private function loadStripeAPILibraries() { 375 + $root = dirname(phutil_get_library_root('phabricator')); 376 + require_once $root.'/externals/stripe-php/lib/Stripe.php'; 363 377 } 364 378 365 379 }
+4
src/applications/phortune/provider/PhortuneTestPaymentProvider.php
··· 62 62 return; 63 63 } 64 64 65 + public function updateCharge(PhortuneCharge $charge) { 66 + return; 67 + } 68 + 65 69 public function getAllConfigurableProperties() { 66 70 return array(); 67 71 }
+35 -22
src/applications/phortune/provider/PhortuneWePayPaymentProvider.php
··· 49 49 } 50 50 51 51 public function runConfigurationTest() { 52 - $root = dirname(phutil_get_library_root('phabricator')); 53 - require_once $root.'/externals/wepay/wepay.php'; 52 + $this->loadWePayAPILibraries(); 54 53 55 54 WePay::useStaging( 56 55 $this->getWePayClientID(), ··· 189 188 protected function executeRefund( 190 189 PhortuneCharge $charge, 191 190 PhortuneCharge $refund) { 191 + $wepay = $this->loadWePayAPILibraries(); 192 192 193 - $root = dirname(phutil_get_library_root('phabricator')); 194 - require_once $root.'/externals/wepay/wepay.php'; 195 - 196 - WePay::useStaging( 197 - $this->getWePayClientID(), 198 - $this->getWePayClientSecret()); 199 - 200 - $wepay = new WePay($this->getWePayAccessToken()); 201 - 202 - $charge_id = $charge->getMetadataValue('wepay.checkoutID'); 193 + $checkout_id = $this->getWePayCheckoutID($charge); 203 194 204 195 $params = array( 205 - 'checkout_id' => $charge_id, 196 + 'checkout_id' => $checkout_id, 206 197 'refund_reason' => pht('Refund'), 207 198 'amount' => $refund->getAmountAsCurrency()->negate()->formatBareValue(), 208 199 ); 209 200 210 201 $wepay->request('checkout/refund', $params); 211 202 } 203 + 204 + public function updateCharge(PhortuneCharge $charge) { 205 + $wepay = $this->loadWePayAPILibraries(); 206 + 207 + $params = array( 208 + 'checkout_id' => $this->getWePayCheckoutID($charge), 209 + ); 210 + $wepay_checkout = $wepay->request('checkout', $params); 211 + 212 + // TODO: Deal with disputes / chargebacks / surprising refunds. 213 + } 214 + 212 215 213 216 /* -( One-Time Payments )-------------------------------------------------- */ 214 217 ··· 236 239 public function processControllerRequest( 237 240 PhortuneProviderActionController $controller, 238 241 AphrontRequest $request) { 242 + $wepay = $this->loadWePayAPILibraries(); 239 243 240 244 $viewer = $request->getUser(); 241 245 ··· 243 247 if (!$cart) { 244 248 return new Aphront404Response(); 245 249 } 246 - 247 - $root = dirname(phutil_get_library_root('phabricator')); 248 - require_once $root.'/externals/wepay/wepay.php'; 249 - 250 - WePay::useStaging( 251 - $this->getWePayClientID(), 252 - $this->getWePayClientSecret()); 253 - 254 - $wepay = new WePay($this->getWePayAccessToken()); 255 250 256 251 $charge = $controller->loadActiveCharge($cart); 257 252 switch ($controller->getAction()) { ··· 388 383 pht('Unsupported action "%s".', $controller->getAction())); 389 384 } 390 385 386 + private function loadWePayAPILibraries() { 387 + $root = dirname(phutil_get_library_root('phabricator')); 388 + require_once $root.'/externals/wepay/wepay.php'; 389 + 390 + WePay::useStaging( 391 + $this->getWePayClientID(), 392 + $this->getWePayClientSecret()); 393 + 394 + return new WePay($this->getWePayAccessToken()); 395 + } 396 + 397 + private function getWePayCheckoutID(PhortuneCharge $charge) { 398 + $checkout_id = $charge->getMetadataValue('wepay.checkoutID'); 399 + if ($checkout_id === null) { 400 + throw new Exception(pht('No WePay Checkout ID present on charge!')); 401 + } 402 + return $checkout_id; 403 + } 391 404 392 405 }
+8 -10
src/applications/phortune/storage/PhortuneCart.php
··· 149 149 $copy = clone $this; 150 150 $copy->reload(); 151 151 152 - if ($copy->getStatus() !== self::STATUS_PURCHASING) { 152 + if (($copy->getStatus() !== self::STATUS_PURCHASING) && 153 + ($copy->getStatus() !== self::STATUS_HOLD)) { 153 154 throw new Exception( 154 155 pht( 155 - 'Cart has wrong status ("%s") to call didApplyCharge(), '. 156 - 'expected "%s".', 157 - $copy->getStatus(), 158 - self::STATUS_PURCHASING)); 156 + 'Cart has wrong status ("%s") to call didApplyCharge().', 157 + $copy->getStatus())); 159 158 } 160 159 161 160 $charge->save(); ··· 182 181 $copy = clone $this; 183 182 $copy->reload(); 184 183 185 - if ($copy->getStatus() !== self::STATUS_PURCHASING) { 184 + if (($copy->getStatus() !== self::STATUS_PURCHASING) && 185 + ($copy->getStatus() !== self::STATUS_HOLD)) { 186 186 throw new Exception( 187 187 pht( 188 - 'Cart has wrong status ("%s") to call didFailCharge(), '. 189 - 'expected "%s".', 190 - $copy->getStatus(), 191 - self::STATUS_PURCHASING)); 188 + 'Cart has wrong status ("%s") to call didFailCharge().', 189 + $copy->getStatus())); 192 190 } 193 191 194 192 $charge->save();
+4
src/applications/phortune/storage/PhortuneCharge.php
··· 84 84 return idx(self::getStatusNameMap(), $status, pht('Unknown')); 85 85 } 86 86 87 + public function isRefund() { 88 + return $this->getAmountAsCurrency()->negate()->isPositive(); 89 + } 90 + 87 91 public function getStatusForDisplay() { 88 92 if ($this->getStatus() == self::STATUS_CHARGED) { 89 93 if ($this->getRefundedChargePHID()) {