@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
at recaptime-dev/main 462 lines 14 kB view raw
1<?php 2 3final class PhortunePaymentMethodCreateController 4 extends PhortuneController { 5 6 public function handleRequest(AphrontRequest $request) { 7 $viewer = $request->getViewer(); 8 9 $account_id = $request->getURIData('accountID'); 10 $account = id(new PhortuneAccountQuery()) 11 ->setViewer($viewer) 12 ->withIDs(array($account_id)) 13 ->requireCapabilities( 14 array( 15 PhabricatorPolicyCapability::CAN_VIEW, 16 PhabricatorPolicyCapability::CAN_EDIT, 17 )) 18 ->executeOne(); 19 if (!$account) { 20 return new Aphront404Response(); 21 } 22 23 $cart_id = $request->getInt('cartID'); 24 $subscription_id = $request->getInt('subscriptionID'); 25 $merchant_id = $request->getInt('merchantID'); 26 27 if ($cart_id) { 28 $cart = id(new PhortuneCartQuery()) 29 ->setViewer($viewer) 30 ->withAccountPHIDs(array($account->getPHID())) 31 ->withIDs(array($cart_id)) 32 ->executeOne(); 33 if (!$cart) { 34 return new Aphront404Response(); 35 } 36 37 $subscription_phid = $cart->getSubscriptionPHID(); 38 if ($subscription_phid) { 39 $subscription = id(new PhortuneSubscriptionQuery()) 40 ->setViewer($viewer) 41 ->withAccountPHIDs(array($account->getPHID())) 42 ->withPHIDs(array($subscription_phid)) 43 ->executeOne(); 44 if (!$subscription) { 45 return new Aphront404Response(); 46 } 47 } else { 48 $subscription = null; 49 } 50 51 $merchant = $cart->getMerchant(); 52 53 $cart_id = $cart->getID(); 54 $subscription_id = null; 55 $merchant_id = null; 56 57 $next_uri = $cart->getCheckoutURI(); 58 } else if ($subscription_id) { 59 $subscription = id(new PhortuneSubscriptionQuery()) 60 ->setViewer($viewer) 61 ->withAccountPHIDs(array($account->getPHID())) 62 ->withIDs(array($subscription_id)) 63 ->executeOne(); 64 if (!$subscription) { 65 return new Aphront404Response(); 66 } 67 68 $cart = null; 69 $merchant = $subscription->getMerchant(); 70 71 $cart_id = null; 72 $subscription_id = $subscription->getID(); 73 $merchant_id = null; 74 75 $next_uri = $subscription->getURI(); 76 } else if ($merchant_id) { 77 $merchant_phids = $account->getMerchantPHIDs(); 78 if ($merchant_phids) { 79 $merchant = id(new PhortuneMerchantQuery()) 80 ->setViewer($viewer) 81 ->withIDs(array($merchant_id)) 82 ->withPHIDs($merchant_phids) 83 ->executeOne(); 84 } else { 85 $merchant = null; 86 } 87 88 if (!$merchant) { 89 return new Aphront404Response(); 90 } 91 92 $cart = null; 93 $subscription = null; 94 95 $cart_id = null; 96 $subscription_id = null; 97 $merchant_id = $merchant->getID(); 98 99 $next_uri = $account->getPaymentMethodsURI(); 100 } else { 101 $next_uri = $account->getPaymentMethodsURI(); 102 103 $merchant_phids = $account->getMerchantPHIDs(); 104 if ($merchant_phids) { 105 $merchants = id(new PhortuneMerchantQuery()) 106 ->setViewer($viewer) 107 ->withPHIDs($merchant_phids) 108 ->needProfileImage(true) 109 ->execute(); 110 } else { 111 $merchants = array(); 112 } 113 114 if (!$merchants) { 115 return $this->newDialog() 116 ->setTitle(pht('No Merchants')) 117 ->appendParagraph( 118 pht( 119 'You have not established a relationship with any merchants '. 120 'yet. Create an order or subscription before adding payment '. 121 'methods.')) 122 ->addCancelButton($next_uri); 123 } 124 125 // If there's more than one merchant, ask the user to pick which one they 126 // want to pay. If there's only one, just pick it for them. 127 if (count($merchants) > 1) { 128 $menu = $this->newMerchantMenu($merchants); 129 130 $form = id(new AphrontFormView()) 131 ->appendInstructions( 132 pht( 133 'Choose the merchant you want to pay.')); 134 135 return $this->newDialog() 136 ->setTitle(pht('Choose a Merchant')) 137 ->appendForm($form) 138 ->appendChild($menu) 139 ->addCancelButton($next_uri); 140 } 141 142 $cart = null; 143 $subscription = null; 144 $merchant = head($merchants); 145 146 $cart_id = null; 147 $subscription_id = null; 148 $merchant_id = $merchant->getID(); 149 } 150 151 $providers = $this->loadCreatePaymentMethodProvidersForMerchant($merchant); 152 if (!$providers) { 153 throw new Exception( 154 pht( 155 'There are no payment providers enabled that can add payment '. 156 'methods.')); 157 } 158 159 $state_params = array( 160 'cartID' => $cart_id, 161 'subscriptionID' => $subscription_id, 162 'merchantID' => $merchant_id, 163 ); 164 $state_params = array_filter($state_params); 165 166 $state_uri = new PhutilURI($request->getRequestURI()); 167 foreach ($state_params as $key => $value) { 168 $state_uri->replaceQueryParam($key, $value); 169 } 170 171 $provider_id = $request->getInt('providerID'); 172 if (isset($providers[$provider_id])) { 173 $provider = $providers[$provider_id]; 174 } else { 175 // If there's more than one provider, ask the user to pick how they 176 // want to pay. If there's only one, just pick it. 177 if (count($providers) > 1) { 178 $menu = $this->newProviderMenu($providers, $state_uri); 179 180 return $this->newDialog() 181 ->setTitle(pht('Choose a Payment Method')) 182 ->appendChild($menu) 183 ->addCancelButton($next_uri); 184 } 185 186 $provider = head($providers); 187 } 188 189 $provider_id = $provider->getProviderConfig()->getID(); 190 191 $state_params['providerID'] = $provider_id; 192 193 $errors = array(); 194 $display_exception = null; 195 if ($request->isFormPost() && $request->getBool('isProviderForm')) { 196 $method = id(new PhortunePaymentMethod()) 197 ->setAccountPHID($account->getPHID()) 198 ->setAuthorPHID($viewer->getPHID()) 199 ->setMerchantPHID($merchant->getPHID()) 200 ->setProviderPHID($provider->getProviderConfig()->getPHID()) 201 ->setStatus(PhortunePaymentMethod::STATUS_ACTIVE); 202 203 // Limit the rate at which you can attempt to add payment methods. This 204 // is intended as a line of defense against using Phortune to validate a 205 // large list of stolen credit card numbers. 206 207 PhabricatorSystemActionEngine::willTakeAction( 208 array($viewer->getPHID()), 209 new PhortuneAddPaymentMethodAction(), 210 1); 211 212 if (!$errors) { 213 $errors = $this->processClientErrors( 214 $provider, 215 $request->getStr('errors')); 216 } 217 218 if (!$errors) { 219 $client_token_raw = $request->getStr('token'); 220 $client_token = null; 221 try { 222 $client_token = phutil_json_decode($client_token_raw); 223 } catch (PhutilJSONParserException $ex) { 224 $errors[] = pht( 225 'There was an error decoding token information submitted by the '. 226 'client. Expected a JSON-encoded token dictionary, received: %s.', 227 nonempty($client_token_raw, pht('nothing'))); 228 } 229 230 if (!$provider->validateCreatePaymentMethodToken($client_token)) { 231 $errors[] = pht( 232 'There was an error with the payment token submitted by the '. 233 'client. Expected a valid dictionary, received: %s.', 234 $client_token_raw); 235 } 236 237 if (!$errors) { 238 try { 239 $provider->createPaymentMethodFromRequest( 240 $request, 241 $method, 242 $client_token); 243 } catch (PhortuneDisplayException $exception) { 244 $display_exception = $exception; 245 } catch (Exception $ex) { 246 $errors = array( 247 pht('There was an error adding this payment method:'), 248 $ex->getMessage(), 249 ); 250 } 251 } 252 } 253 254 if (!$errors && !$display_exception) { 255 $xactions = array(); 256 257 $xactions[] = $method->getApplicationTransactionTemplate() 258 ->setTransactionType(PhabricatorTransactions::TYPE_CREATE) 259 ->setNewValue(true); 260 261 $editor = id(new PhortunePaymentMethodEditor()) 262 ->setActor($viewer) 263 ->setContentSourceFromRequest($request) 264 ->setContinueOnNoEffect(true) 265 ->setContinueOnMissingFields(true); 266 267 $editor->applyTransactions($method, $xactions); 268 269 $next_uri = new PhutilURI($next_uri); 270 271 // If we added this method on a cart flow, return to the cart to 272 // checkout with this payment method selected. 273 if ($cart_id) { 274 $next_uri->replaceQueryParam('paymentMethodID', $method->getID()); 275 } 276 277 return id(new AphrontRedirectResponse())->setURI($next_uri); 278 } else { 279 if ($display_exception) { 280 $dialog_body = $display_exception->getView(); 281 } else { 282 $dialog_body = id(new PHUIInfoView()) 283 ->setErrors($errors); 284 } 285 286 return $this->newDialog() 287 ->setTitle(pht('Error Adding Payment Method')) 288 ->appendChild($dialog_body) 289 ->addCancelButton($request->getRequestURI()); 290 } 291 } 292 293 $form = $provider->renderCreatePaymentMethodForm($request, $errors); 294 295 $form 296 ->setViewer($viewer) 297 ->setAction($request->getPath()) 298 ->setWorkflow(true) 299 ->addHiddenInput('isProviderForm', true) 300 ->appendChild( 301 id(new AphrontFormSubmitControl()) 302 ->setValue(pht('Add Payment Method')) 303 ->addCancelButton($next_uri)); 304 305 foreach ($state_params as $key => $value) { 306 $form->addHiddenInput($key, $value); 307 } 308 309 $box = id(new PHUIObjectBoxView()) 310 ->setHeaderText(pht('Method')) 311 ->setBackground(PHUIObjectBoxView::BLUE_PROPERTY) 312 ->setForm($form); 313 314 $crumbs = $this->buildApplicationCrumbs() 315 ->addTextCrumb(pht('Add Payment Method')) 316 ->setBorder(true); 317 318 $header = id(new PHUIHeaderView()) 319 ->setHeader(pht('Add Payment Method')) 320 ->setHeaderIcon('fa-plus-square'); 321 322 $view = id(new PHUITwoColumnView()) 323 ->setHeader($header) 324 ->setFooter( 325 array( 326 $box, 327 )); 328 329 return $this->newPage() 330 ->setTitle($provider->getPaymentMethodDescription()) 331 ->setCrumbs($crumbs) 332 ->appendChild($view); 333 334 } 335 336 private function processClientErrors( 337 PhortunePaymentProvider $provider, 338 $client_errors_raw) { 339 340 $errors = array(); 341 342 $client_errors = null; 343 try { 344 $client_errors = phutil_json_decode($client_errors_raw); 345 } catch (PhutilJSONParserException $ex) { 346 $errors[] = pht( 347 'There was an error decoding error information submitted by the '. 348 'client. Expected a JSON-encoded list of error codes, received: %s.', 349 nonempty($client_errors_raw, pht('nothing'))); 350 } 351 352 foreach (array_unique($client_errors) as $key => $client_error) { 353 $client_errors[$key] = $provider->translateCreatePaymentMethodErrorCode( 354 $client_error); 355 } 356 357 foreach (array_unique($client_errors) as $client_error) { 358 switch ($client_error) { 359 case PhortuneErrCode::ERR_CC_INVALID_NUMBER: 360 $message = pht( 361 'The card number you entered is not a valid card number. Check '. 362 'that you entered it correctly.'); 363 break; 364 case PhortuneErrCode::ERR_CC_INVALID_CVC: 365 $message = pht( 366 'The CVC code you entered is not a valid CVC code. Check that '. 367 'you entered it correctly. The CVC code is a 3-digit or 4-digit '. 368 'numeric code which usually appears on the back of the card.'); 369 break; 370 case PhortuneErrCode::ERR_CC_INVALID_EXPIRY: 371 $message = pht( 372 'The card expiration date is not a valid expiration date. Check '. 373 'that you entered it correctly. You can not add an expired card '. 374 'as a payment method.'); 375 break; 376 default: 377 $message = $provider->getCreatePaymentMethodErrorMessage( 378 $client_error); 379 if (!$message) { 380 $message = pht( 381 "There was an unexpected error ('%s') processing payment ". 382 "information.", 383 $client_error); 384 385 phlog($message); 386 } 387 break; 388 } 389 390 $errors[$client_error] = $message; 391 } 392 393 return $errors; 394 } 395 396 private function newMerchantMenu(array $merchants) { 397 assert_instances_of($merchants, 'PhortuneMerchant'); 398 399 $request = $this->getRequest(); 400 $viewer = $this->getViewer(); 401 402 $menu = id(new PHUIObjectItemListView()) 403 ->setUser($viewer) 404 ->setBig(true) 405 ->setFlush(true); 406 407 foreach ($merchants as $merchant) { 408 $merchant_uri = id(new PhutilURI($request->getRequestURI())) 409 ->replaceQueryParam('merchantID', $merchant->getID()); 410 411 $item = id(new PHUIObjectItemView()) 412 ->setObjectName($merchant->getObjectName()) 413 ->setHeader($merchant->getName()) 414 ->setHref($merchant_uri) 415 ->setClickable(true) 416 ->setImageURI($merchant->getProfileImageURI()); 417 418 $menu->addItem($item); 419 } 420 421 return $menu; 422 } 423 424 private function newProviderMenu(array $providers, PhutilURI $state_uri) { 425 assert_instances_of($providers, 'PhortunePaymentProvider'); 426 427 $request = $this->getRequest(); 428 $viewer = $this->getViewer(); 429 430 $menu = id(new PHUIObjectItemListView()) 431 ->setUser($viewer) 432 ->setBig(true) 433 ->setFlush(true); 434 435 foreach ($providers as $provider) { 436 $provider_id = $provider->getProviderConfig()->getID(); 437 438 $provider_uri = id(clone $state_uri) 439 ->replaceQueryParam('providerID', $provider_id); 440 441 $description = $provider->getPaymentMethodDescription(); 442 $icon_uri = $provider->getPaymentMethodIcon(); 443 $details = $provider->getPaymentMethodProviderDescription(); 444 445 $icon = id(new PHUIIconView()) 446 ->setSpriteSheet(PHUIIconView::SPRITE_LOGIN) 447 ->setSpriteIcon($icon_uri); 448 449 $item = id(new PHUIObjectItemView()) 450 ->setHeader($description) 451 ->setHref($provider_uri) 452 ->setClickable(true) 453 ->addAttribute($details) 454 ->setImageIcon($icon); 455 456 $menu->addItem($item); 457 } 458 459 return $menu; 460 } 461 462}