Passed
Push — master ( 489f2e...0cdf36 )
by
unknown
289:17 queued 286:28
created

AddDonationController::newHttpResponse()   B

Complexity

Conditions 6
Paths 6

Size

Total Lines 39
Code Lines 29

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 6
eloc 29
nc 6
nop 1
dl 0
loc 39
rs 8.8337
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 Silex\Application;
8
use Symfony\Component\HttpFoundation\RedirectResponse;
9
use Symfony\Component\HttpFoundation\Request;
10
use Symfony\Component\HttpFoundation\Response;
11
use Symfony\Component\HttpFoundation\Session\SessionInterface;
12
use WMDE\Euro\Euro;
13
use WMDE\Fundraising\DonationContext\Domain\Model\DonationTrackingInfo;
14
use WMDE\Fundraising\DonationContext\Domain\Model\DonorType;
15
use WMDE\Fundraising\DonationContext\UseCases\AddDonation\AddDonationRequest;
16
use WMDE\Fundraising\DonationContext\UseCases\AddDonation\AddDonationResponse;
17
use WMDE\Fundraising\Frontend\BucketTesting\Logging\Events\DonationCreated;
18
use WMDE\Fundraising\Frontend\Factories\FunFunFactory;
19
use WMDE\Fundraising\Frontend\Infrastructure\AddressType;
20
use WMDE\Fundraising\Frontend\Infrastructure\Validation\FallbackRequestValueReader;
21
use WMDE\Fundraising\PaymentContext\Domain\Model\BankData;
22
use WMDE\Fundraising\PaymentContext\Domain\Model\Iban;
23
use WMDE\Fundraising\PaymentContext\Domain\Model\PaymentMethod;
24
use WMDE\FunValidators\ConstraintViolation;
25
26
/**
27
 * @license GPL-2.0-or-later
28
 */
29
class AddDonationController {
30
31
	private SessionInterface $session;
32
	private FunFunFactory $ffFactory;
33
	/**
34
	 * @var FallbackRequestValueReader
35
	 */
36
	private FallbackRequestValueReader $legacyRequestValueReader;
37
38
	public function index( FunFunFactory $ffFactory, Request $request, SessionInterface $session ): Response {
39
		$this->session = $session;
40
		$this->ffFactory = $ffFactory;
41
		if ( !$this->isSubmissionAllowed( $request ) ) {
42
			return new Response( $this->ffFactory->newSystemMessageResponse( 'donation_rejected_limit' ) );
43
		}
44
45
		$this->legacyRequestValueReader = new FallbackRequestValueReader( $ffFactory->getLogger(), $request );
46
		$addDonationRequest = $this->createDonationRequest( $request );
47
		$responseModel = $this->ffFactory->newAddDonationUseCase()->addDonation( $addDonationRequest );
48
49
		if ( !$responseModel->isSuccessful() ) {
50
			$this->logValidationErrors( $responseModel->getValidationErrors() );
51
			return new Response(
52
				$this->ffFactory->newDonationFormViolationPresenter()->present(
53
					$responseModel->getValidationErrors(),
54
					$addDonationRequest,
55
					$this->newTrackingInfoFromRequest( $request )
56
				)
57
			);
58
		}
59
60
		$this->sendTrackingDataIfNeeded( $request, $responseModel );
61
		$this->resetSessionState();
62
63
		return $this->newHttpResponse( $responseModel );
64
	}
65
66
	private function sendTrackingDataIfNeeded( Request $request, AddDonationResponse $responseModel ) {
67
		if ( $request->get( 'mbt', '' ) !== '1' || !$responseModel->getDonation()->hasExternalPayment() ) {
68
			return;
69
		}
70
71
		$trackingCode = explode( '/', $request->attributes->get( 'trackingCode' ) );
72
		$campaign = $trackingCode[0];
73
		$keyword = $trackingCode[1] ?? '';
74
75
		$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

75
		$this->ffFactory->getPageViewTracker()->trackPaypalRedirection( $campaign, $keyword, /** @scrutinizer ignore-type */ $request->getClientIp() );
Loading history...
76
	}
77
78
	private function newHttpResponse( AddDonationResponse $responseModel ): Response {
79
		switch ( $responseModel->getDonation()->getPaymentMethodId() ) {
80
			case PaymentMethod::DIRECT_DEBIT:
81
			case PaymentMethod::BANK_TRANSFER:
82
				return new RedirectResponse(
83
					$this->ffFactory->getUrlGenerator()->generateAbsoluteUrl(
84
						'show-donation-confirmation',
85
						[
86
							'id' => $responseModel->getDonation()->getId(),
87
							'accessToken' => $responseModel->getAccessToken()
88
						]
89
					)
90
				);
91
			case PaymentMethod::PAYPAL:
92
				return new RedirectResponse(
93
					$this->ffFactory->newPayPalUrlGeneratorForDonations()->generateUrl(
94
						$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

94
						/** @scrutinizer ignore-type */ $responseModel->getDonation()->getId(),
Loading history...
95
						$responseModel->getDonation()->getAmount(),
96
						$responseModel->getDonation()->getPaymentIntervalInMonths(),
97
						$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

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

98
						/** @scrutinizer ignore-type */ $responseModel->getAccessToken()
Loading history...
99
					)
100
				);
101
			case PaymentMethod::SOFORT:
102
				return new RedirectResponse(
103
					$this->ffFactory->newSofortUrlGeneratorForDonations()->generateUrl(
104
						$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

104
						/** @scrutinizer ignore-type */ $responseModel->getDonation()->getId(),
Loading history...
105
						$responseModel->getDonation()->getPayment()->getPaymentMethod()->getBankTransferCode(),
106
						$responseModel->getDonation()->getAmount(),
107
						$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

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

108
						/** @scrutinizer ignore-type */ $responseModel->getAccessToken()
Loading history...
109
					)
110
				);
111
			case PaymentMethod::CREDIT_CARD:
112
				return new RedirectResponse(
113
					$this->ffFactory->newCreditCardPaymentUrlGenerator()->buildUrl( $responseModel )
114
				);
115
			default:
116
				throw new \LogicException( 'This code should not be reached' );
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
		$donationRequest->setSource( $request->attributes->get( 'trackingSource' ) );
147
		$donationRequest->setTotalImpressionCount( intval( $request->get( 'impCount', 0 ) ) );
148
		$donationRequest->setSingleBannerImpressionCount( intval( $request->get( 'bImpCount', 0 ) ) );
149
		$donationRequest->setOptsIntoDonationReceipt( $request->request->getBoolean( 'donationReceipt', true ) );
150
151
		return $donationRequest;
152
	}
153
154
	private function getBankDataFromRequest( Request $request ): BankData {
155
		$bankData = new BankData();
156
		$bankData->setIban( new Iban( $request->get( 'iban', '' ) ) )
157
			->setBic( $request->get( 'bic', '' ) )
158
			->setAccount( $request->get( 'konto', '' ) )
159
			->setBankCode( $request->get( 'blz', '' ) )
160
			->setBankName( $request->get( 'bankname', '' ) );
161
162
		if ( $bankData->isComplete() ) {
163
			return $bankData->freeze()->assertNoNullFields();
164
		}
165
166
		if ( $bankData->hasIban() ) {
167
			$bankData = $this->newBankDataFromIban( $bankData->getIban() );
168
		} elseif ( $bankData->hasCompleteLegacyBankData() ) {
169
			$bankData = $this->newBankDataFromAccountAndBankCode( $bankData->getAccount(), $bankData->getBankCode() );
170
		}
171
		return $bankData->freeze()->assertNoNullFields();
172
	}
173
174
	private function newBankDataFromIban( Iban $iban ): BankData {
175
		return $this->ffFactory->newBankDataConverter()->getBankDataFromIban( $iban );
176
	}
177
178
	private function newBankDataFromAccountAndBankCode( string $account, string $bankCode ): BankData {
179
		return $this->ffFactory->newBankDataConverter()->getBankDataFromAccountData( $account, $bankCode );
180
	}
181
182
	private function getEuroAmount( int $amount ): Euro {
183
		try {
184
			return Euro::newFromCents( $amount );
185
		} catch ( \InvalidArgumentException $ex ) {
186
			return Euro::newFromCents( 0 );
187
		}
188
	}
189
190
	private function getAmountFromRequest( Request $request ): int {
191
		if ( $request->request->has( 'amount' ) ) {
192
			return intval( $request->get( 'amount' ) );
193
		}
194
		return $this->legacyRequestValueReader->getAmount();
195
	}
196
197
	private function isSubmissionAllowed( Request $request ): bool {
198
		$lastSubmission = $request->cookies->get( ShowDonationConfirmationController::SUBMISSION_COOKIE_NAME, '' );
199
		if ( $lastSubmission === '' ) {
200
			return true;
201
		}
202
203
		$minNextTimestamp =
204
			\DateTime::createFromFormat( ShowDonationConfirmationController::TIMESTAMP_FORMAT, $lastSubmission )
205
			->add( new \DateInterval( $this->ffFactory->getDonationTimeframeLimit() ) );
206
207
		if ( $minNextTimestamp > new \DateTime() ) {
208
			return false;
209
		}
210
211
		return true;
212
	}
213
214
	private function newTrackingInfoFromRequest( Request $request ): DonationTrackingInfo {
215
		$tracking = new DonationTrackingInfo();
216
		$tracking->setSingleBannerImpressionCount( intval( $request->get( 'bImpCount', 0 ) ) );
217
		$tracking->setTotalImpressionCount( intval( $request->get( 'impCount', 0 ) ) );
218
219
		return $tracking;
220
	}
221
222
	/**
223
	 * Safari and Chrome concatenate street autofill values (e.g. house number and street name) with a comma.
224
	 * This method removes the commas.
225
	 *
226
	 * @param string $value
227
	 * @return string
228
	 */
229
	private function filterAutofillCommas( string $value ): string {
230
		return trim( preg_replace( [ '/,/', '/\s{2,}/' ], [ ' ', ' ' ], $value ) );
231
	}
232
233
	/**
234
	 * Reset session data to prevent old donations from changing the application output due to old data leaking into the new session
235
	 */
236
	private function resetSessionState(): void {
237
		$this->session->set(
238
			UpdateDonorController::ADDRESS_CHANGE_SESSION_KEY,
239
			false
240
		);
241
	}
242
243
	/**
244
	 * @param ConstraintViolation[] $constraintViolations
245
	 */
246
	private function logValidationErrors( array $constraintViolations ): void {
247
		$fields = [];
248
		$formattedConstraintViolations = [];
249
		foreach ( $constraintViolations as $constraintViolation ) {
250
			$source = $constraintViolation->getSource();
251
			$fields[] = $source;
252
			$formattedConstraintViolations['validation_errors'][] = sprintf(
253
				'Validation field "%s" with value "%s" failed with: %s',
254
				$source,
255
				$constraintViolation->getValue(),
256
				$constraintViolation->getMessageIdentifier()
257
			);
258
		}
259
260
		$this->ffFactory->getValidationErrorLogger()->logViolations(
261
			'Unexpected server-side form validation errors.',
262
			$fields,
263
			$formattedConstraintViolations
264
		);
265
	}
266
267
	/**
268
	 * Get AddDonationRequest donor type from HTTP request field.
269
	 *
270
	 * Assumes "Anonymous" when field is not set or invalid.
271
	 *
272
	 * @param Request $request
273
	 *
274
	 * @return DonorType
275
	 */
276
	private function getSafeDonorType( Request $request ): DonorType {
277
		try {
278
			return DonorType::make(
279
				AddressType::presentationAddressTypeToDomainAddressType( $request->get( 'addressType', '' ) )
280
			);
281
		} catch ( \UnexpectedValueException $e ) {
282
			return DonorType::ANONYMOUS();
283
		}
284
	}
285
}
286