Passed
Pull Request — master (#2014)
by
unknown
60:59
created

AddDonationController   A

Complexity

Total Complexity 31

Size/Duplication

Total Lines 240
Duplicated Lines 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 130
c 1
b 0
f 0
dl 0
loc 240
rs 9.92
wmc 31

14 Methods

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