@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<?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}