Passed
Pull Request — master (#2014)
by Gabriel
125:39 queued 60:36
created

newTrackingInfoFromRequest()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 6
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 4
nc 1
nop 1
dl 0
loc 6
rs 10
c 0
b 0
f 0
1
<?php
2
3
declare( strict_types = 1 );
4
5
namespace WMDE\Fundraising\Frontend\App\Controllers\Donation;
6
7
use Symfony\Component\HttpFoundation\RedirectResponse;
8
use Symfony\Component\HttpFoundation\Request;
9
use Symfony\Component\HttpFoundation\Response;
10
use Symfony\Component\HttpFoundation\Session\SessionInterface;
11
use WMDE\Euro\Euro;
12
use WMDE\Fundraising\DonationContext\Domain\Model\DonationTrackingInfo;
13
use WMDE\Fundraising\DonationContext\Domain\Model\DonorType;
14
use WMDE\Fundraising\DonationContext\UseCases\AddDonation\AddDonationRequest;
15
use WMDE\Fundraising\DonationContext\UseCases\AddDonation\AddDonationResponse;
16
use WMDE\Fundraising\Frontend\Factories\FunFunFactory;
17
use WMDE\Fundraising\Frontend\Infrastructure\AddressType;
18
use WMDE\Fundraising\Frontend\Infrastructure\Validation\FallbackRequestValueReader;
19
use WMDE\Fundraising\PaymentContext\Domain\Model\BankData;
20
use WMDE\Fundraising\PaymentContext\Domain\Model\Iban;
21
use WMDE\Fundraising\PaymentContext\Domain\Model\PaymentMethod;
22
use WMDE\FunValidators\ConstraintViolation;
23
24
/**
25
 * @license GPL-2.0-or-later
26
 */
27
class AddDonationController {
28
29
	private SessionInterface $session;
30
	private FunFunFactory $ffFactory;
31
	/**
32
	 * @var FallbackRequestValueReader
33
	 */
34
	private FallbackRequestValueReader $legacyRequestValueReader;
35
36
	public function index( FunFunFactory $ffFactory, Request $request, SessionInterface $session ): Response {
37
		$this->session = $session;
38
		$this->ffFactory = $ffFactory;
39
		if ( !$ffFactory->getDonationSubmissionRateLimiter()->isSubmissionAllowed( $session ) ) {
40
			return new Response( $this->ffFactory->newSystemMessageResponse( 'donation_rejected_limit' ) );
41
		}
42
43
		$this->legacyRequestValueReader = new FallbackRequestValueReader( $ffFactory->getLogger(), $request );
44
		$addDonationRequest = $this->createDonationRequest( $request );
45
		$responseModel = $this->ffFactory->newAddDonationUseCase()->addDonation( $addDonationRequest );
46
47
		if ( !$responseModel->isSuccessful() ) {
48
			$this->logValidationErrors( $responseModel->getValidationErrors() );
49
			return new Response(
50
				$this->ffFactory->newDonationFormViolationPresenter()->present(
51
					$responseModel->getValidationErrors(),
52
					$addDonationRequest,
53
					$this->newTrackingInfoFromRequest( $request )
54
				)
55
			);
56
		}
57
58
		$this->sendTrackingDataIfNeeded( $request, $responseModel );
59
		$this->resetAddressChangeDataInSession();
60
61
		return $this->newHttpResponse( $session, $responseModel );
62
	}
63
64
	private function sendTrackingDataIfNeeded( Request $request, AddDonationResponse $responseModel ) {
65
		if ( $request->get( 'mbt', '' ) !== '1' || !$responseModel->getDonation()->hasExternalPayment() ) {
66
			return;
67
		}
68
69
		$trackingCode = explode( '/', $request->attributes->get( 'trackingCode' ) );
70
		$campaign = $trackingCode[0];
71
		$keyword = $trackingCode[1] ?? '';
72
73
		$this->ffFactory->getPageViewTracker()->trackPaypalRedirection( $campaign, $keyword, $request->getClientIp() );
0 ignored issues
show
Bug introduced by
It seems like $request->getClientIp() can also be of type null; however, parameter $visitorIP of WMDE\Fundraising\Fronten...rackPaypalRedirection() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

73
		$this->ffFactory->getPageViewTracker()->trackPaypalRedirection( $campaign, $keyword, /** @scrutinizer ignore-type */ $request->getClientIp() );
Loading history...
74
	}
75
76
	private function newHttpResponse( SessionInterface $session, AddDonationResponse $responseModel ): Response {
77
		$this->ffFactory->getDonationSubmissionRateLimiter()->setRateLimitCookie( $session );
78
		switch ( $responseModel->getDonation()->getPaymentMethodId() ) {
79
			case PaymentMethod::DIRECT_DEBIT:
80
			case PaymentMethod::BANK_TRANSFER:
81
				return new RedirectResponse(
82
					$this->ffFactory->getUrlGenerator()->generateAbsoluteUrl(
83
						'show-donation-confirmation',
84
						[
85
							'id' => $responseModel->getDonation()->getId(),
86
							'accessToken' => $responseModel->getAccessToken()
87
						]
88
					)
89
				);
90
			case PaymentMethod::PAYPAL:
91
				return new RedirectResponse(
92
					$this->ffFactory->newPayPalUrlGeneratorForDonations()->generateUrl(
93
						$responseModel->getDonation()->getId(),
0 ignored issues
show
Bug introduced by
It seems like $responseModel->getDonation()->getId() can also be of type null; however, parameter $itemId of WMDE\Fundraising\Payment...r\PayPal::generateUrl() does only seem to accept integer, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

93
						/** @scrutinizer ignore-type */ $responseModel->getDonation()->getId(),
Loading history...
94
						$responseModel->getDonation()->getAmount(),
95
						$responseModel->getDonation()->getPaymentIntervalInMonths(),
96
						$responseModel->getUpdateToken(),
0 ignored issues
show
Bug introduced by
It seems like $responseModel->getUpdateToken() can also be of type null; however, parameter $updateToken of WMDE\Fundraising\Payment...r\PayPal::generateUrl() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

96
						/** @scrutinizer ignore-type */ $responseModel->getUpdateToken(),
Loading history...
97
						$responseModel->getAccessToken()
0 ignored issues
show
Bug introduced by
It seems like $responseModel->getAccessToken() can also be of type null; however, parameter $accessToken of WMDE\Fundraising\Payment...r\PayPal::generateUrl() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

97
						/** @scrutinizer ignore-type */ $responseModel->getAccessToken()
Loading history...
98
					)
99
				);
100
			case PaymentMethod::SOFORT:
101
				return new RedirectResponse(
102
					$this->ffFactory->newSofortUrlGeneratorForDonations()->generateUrl(
103
						$responseModel->getDonation()->getId(),
0 ignored issues
show
Bug introduced by
It seems like $responseModel->getDonation()->getId() can also be of type null; however, parameter $internalItemId of WMDE\Fundraising\Payment...r\Sofort::generateUrl() does only seem to accept integer, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

103
						/** @scrutinizer ignore-type */ $responseModel->getDonation()->getId(),
Loading history...
104
						$responseModel->getDonation()->getPayment()->getPaymentMethod()->getBankTransferCode(),
105
						$responseModel->getDonation()->getAmount(),
106
						$responseModel->getUpdateToken(),
0 ignored issues
show
Bug introduced by
It seems like $responseModel->getUpdateToken() can also be of type null; however, parameter $updateToken of WMDE\Fundraising\Payment...r\Sofort::generateUrl() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

106
						/** @scrutinizer ignore-type */ $responseModel->getUpdateToken(),
Loading history...
107
						$responseModel->getAccessToken()
0 ignored issues
show
Bug introduced by
It seems like $responseModel->getAccessToken() can also be of type null; however, parameter $accessToken of WMDE\Fundraising\Payment...r\Sofort::generateUrl() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

107
						/** @scrutinizer ignore-type */ $responseModel->getAccessToken()
Loading history...
108
					)
109
				);
110
			case PaymentMethod::CREDIT_CARD:
111
				return new RedirectResponse(
112
					$this->ffFactory->newCreditCardPaymentUrlGenerator()->buildUrl( $responseModel )
113
				);
114
				break;
0 ignored issues
show
Unused Code introduced by
break is not strictly necessary here and could be removed.

The break statement is not necessary if it is preceded for example by a return statement:

switch ($x) {
    case 1:
        return 'foo';
        break; // This break is not necessary and can be left off.
}

If you would like to keep this construct to be consistent with other case statements, you can safely mark this issue as a false-positive.

Loading history...
115
			default:
116
				throw new \LogicException( 'Unknown Payment method - can\'t determine response' );
117
		}
118
	}
119
120
	private function createDonationRequest( Request $request ): AddDonationRequest {
121
		$donationRequest = new AddDonationRequest();
122
123
		// TODO Replace legacyRequestValueReader with default values after January 2021
124
		$donationRequest->setAmount( $this->getEuroAmount( $this->getAmountFromRequest( $request ) ) );
125
		$donationRequest->setPaymentType( $request->get( 'paymentType', $this->legacyRequestValueReader->getPaymentType() ) );
126
		$donationRequest->setInterval( intval( $request->get( 'interval', $this->legacyRequestValueReader->getInterval() ) ) );
127
128
		$donationRequest->setDonorType( $this->getSafeDonorType( $request ) );
129
		$donationRequest->setDonorSalutation( $request->get( 'salutation', '' ) );
130
		$donationRequest->setDonorTitle( $request->get( 'title', '' ) );
131
		$donationRequest->setDonorCompany( $request->get( 'companyName', '' ) );
132
		$donationRequest->setDonorFirstName( $request->get( 'firstName', '' ) );
133
		$donationRequest->setDonorLastName( $request->get( 'lastName', '' ) );
134
		$donationRequest->setDonorStreetAddress( $this->filterAutofillCommas( $request->get( 'street', '' ) ) );
135
		$donationRequest->setDonorPostalCode( $request->get( 'postcode', '' ) );
136
		$donationRequest->setDonorCity( $request->get( 'city', '' ) );
137
		$donationRequest->setDonorCountryCode( $request->get( 'country', '' ) );
138
		$donationRequest->setDonorEmailAddress( $request->get( 'email', '' ) );
139
140
		if ( $donationRequest->getPaymentType() === PaymentMethod::DIRECT_DEBIT ) {
141
			$donationRequest->setBankData( $this->getBankDataFromRequest( $request ) );
142
		}
143
144
		$donationRequest->setTracking( $request->attributes->get( 'trackingCode' ) );
145
		$donationRequest->setOptIn( $request->get( 'info', '' ) );
146
		// Setting source for completeness sake,
147
		// TODO Remove when  https://phabricator.wikimedia.org/T134327 is done
148
		$donationRequest->setSource( '' );
149
		$donationRequest->setTotalImpressionCount( intval( $request->get( 'impCount', 0 ) ) );
150
		$donationRequest->setSingleBannerImpressionCount( intval( $request->get( 'bImpCount', 0 ) ) );
151
		$donationRequest->setOptsIntoDonationReceipt( $request->request->getBoolean( 'donationReceipt', true ) );
152
153
		return $donationRequest;
154
	}
155
156
	private function getBankDataFromRequest( Request $request ): BankData {
157
		$bankData = new BankData();
158
		$bankData->setIban( new Iban( $request->get( 'iban', '' ) ) )
159
			->setBic( $request->get( 'bic', '' ) )
160
			->setAccount( $request->get( 'konto', '' ) )
161
			->setBankCode( $request->get( 'blz', '' ) )
162
			->setBankName( $request->get( 'bankname', '' ) );
163
164
		if ( $bankData->isComplete() ) {
165
			return $bankData->freeze()->assertNoNullFields();
166
		}
167
168
		if ( $bankData->hasIban() ) {
169
			$bankData = $this->newBankDataFromIban( $bankData->getIban() );
170
		} elseif ( $bankData->hasCompleteLegacyBankData() ) {
171
			$bankData = $this->newBankDataFromAccountAndBankCode( $bankData->getAccount(), $bankData->getBankCode() );
172
		}
173
		return $bankData->freeze()->assertNoNullFields();
174
	}
175
176
	private function newBankDataFromIban( Iban $iban ): BankData {
177
		return $this->ffFactory->newBankDataConverter()->getBankDataFromIban( $iban );
178
	}
179
180
	private function newBankDataFromAccountAndBankCode( string $account, string $bankCode ): BankData {
181
		return $this->ffFactory->newBankDataConverter()->getBankDataFromAccountData( $account, $bankCode );
182
	}
183
184
	private function getEuroAmount( int $amount ): Euro {
185
		try {
186
			return Euro::newFromCents( $amount );
187
		} catch ( \InvalidArgumentException $ex ) {
188
			return Euro::newFromCents( 0 );
189
		}
190
	}
191
192
	private function getAmountFromRequest( Request $request ): int {
193
		if ( $request->request->has( 'amount' ) ) {
194
			return intval( $request->get( 'amount' ) );
195
		}
196
		return $this->legacyRequestValueReader->getAmount();
197
	}
198
199
	private function newTrackingInfoFromRequest( Request $request ): DonationTrackingInfo {
200
		$tracking = new DonationTrackingInfo();
201
		$tracking->setSingleBannerImpressionCount( intval( $request->get( 'bImpCount', 0 ) ) );
202
		$tracking->setTotalImpressionCount( intval( $request->get( 'impCount', 0 ) ) );
203
204
		return $tracking;
205
	}
206
207
	/**
208
	 * Safari and Chrome concatenate street autofill values (e.g. house number and street name) with a comma.
209
	 * This method removes the commas.
210
	 *
211
	 * @param string $value
212
	 * @return string
213
	 */
214
	private function filterAutofillCommas( string $value ): string {
215
		return trim( preg_replace( [ '/,/', '/\s{2,}/' ], [ ' ', ' ' ], $value ) );
216
	}
217
218
	/**
219
	 * Reset session data to prevent old donations from changing the application output due to old data leaking into the new session
220
	 */
221
	private function resetAddressChangeDataInSession(): void {
222
		$this->session->set(
223
			UpdateDonorController::ADDRESS_CHANGE_SESSION_KEY,
224
			false
225
		);
226
	}
227
228
	/**
229
	 * @param ConstraintViolation[] $constraintViolations
230
	 */
231
	private function logValidationErrors( array $constraintViolations ): void {
232
		$fields = [];
233
		$formattedConstraintViolations = [];
234
		foreach ( $constraintViolations as $constraintViolation ) {
235
			$source = $constraintViolation->getSource();
236
			$fields[] = $source;
237
			$formattedConstraintViolations['validation_errors'][] = sprintf(
238
				'Validation field "%s" with value "%s" failed with: %s',
239
				$source,
240
				$constraintViolation->getValue(),
241
				$constraintViolation->getMessageIdentifier()
242
			);
243
		}
244
245
		$this->ffFactory->getValidationErrorLogger()->logViolations(
246
			'Unexpected server-side form validation errors.',
247
			$fields,
248
			$formattedConstraintViolations
249
		);
250
	}
251
252
	/**
253
	 * Get AddDonationRequest donor type from HTTP request field.
254
	 *
255
	 * Assumes "Anonymous" when field is not set or invalid.
256
	 *
257
	 * @param Request $request
258
	 *
259
	 * @return DonorType
260
	 */
261
	private function getSafeDonorType( Request $request ): DonorType {
262
		try {
263
			return DonorType::make(
264
				AddressType::presentationAddressTypeToDomainAddressType( $request->get( 'addressType', '' ) )
265
			);
266
		} catch ( \UnexpectedValueException $e ) {
267
			return DonorType::ANONYMOUS();
268
		}
269
	}
270
}
271