Completed
Pull Request — master (#2015)
by Gabriel
110:58 queued 47:44
created

AddDonationController::sendTrackingDataIfNeeded()   A

Complexity

Conditions 3
Paths 2

Size

Total Lines 10
Code Lines 6

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 3
eloc 6
nc 2
nop 2
dl 0
loc 10
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( $request ) ) {
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->resetSessionState();
59
60
		return $this->newHttpResponse( $request, $responseModel );
61
	}
62
63
	private function newHttpResponse( Request $request, AddDonationResponse $responseModel ): Response {
64
		switch ( $responseModel->getDonation()->getPaymentMethodId() ) {
65
			case PaymentMethod::DIRECT_DEBIT:
66
			case PaymentMethod::BANK_TRANSFER:
67
				$response = new RedirectResponse(
68
					$this->ffFactory->getUrlGenerator()->generateAbsoluteUrl(
69
						'show-donation-confirmation',
70
						[
71
							'id' => $responseModel->getDonation()->getId(),
72
							'accessToken' => $responseModel->getAccessToken()
73
						]
74
					)
75
				);
76
				break;
77
			case PaymentMethod::PAYPAL:
78
				$response = new RedirectResponse(
79
					$this->ffFactory->newPayPalUrlGeneratorForDonations()->generateUrl(
80
						$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

80
						/** @scrutinizer ignore-type */ $responseModel->getDonation()->getId(),
Loading history...
81
						$responseModel->getDonation()->getAmount(),
82
						$responseModel->getDonation()->getPaymentIntervalInMonths(),
83
						$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

83
						/** @scrutinizer ignore-type */ $responseModel->getUpdateToken(),
Loading history...
84
						$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

84
						/** @scrutinizer ignore-type */ $responseModel->getAccessToken()
Loading history...
85
					)
86
				);
87
				break;
88
			case PaymentMethod::SOFORT:
89
				$response = new RedirectResponse(
90
					$this->ffFactory->newSofortUrlGeneratorForDonations()->generateUrl(
91
						$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

91
						/** @scrutinizer ignore-type */ $responseModel->getDonation()->getId(),
Loading history...
92
						$responseModel->getDonation()->getPayment()->getPaymentMethod()->getBankTransferCode(),
93
						$responseModel->getDonation()->getAmount(),
94
						$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

94
						/** @scrutinizer ignore-type */ $responseModel->getUpdateToken(),
Loading history...
95
						$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

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