Completed
Pull Request — master (#650)
by Jeroen De
19:07 queued 01:42
created

handleRequestForDonation()   B

Complexity

Conditions 6
Paths 6

Size

Total Lines 33
Code Lines 20

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 33
rs 8.439
c 0
b 0
f 0
cc 6
eloc 20
nc 6
nop 2
1
<?php
2
3
declare( strict_types = 1 );
4
5
namespace WMDE\Fundraising\Frontend\DonationContext\UseCases\HandlePayPalPaymentNotification;
6
7
use Psr\Log\LoggerInterface;
8
use WMDE\Fundraising\Frontend\DonationContext\Authorization\DonationAuthorizer;
9
use WMDE\Fundraising\Frontend\DonationContext\Domain\Model\Donation;
10
use WMDE\Fundraising\Frontend\DonationContext\Domain\Model\DonationPayment;
11
use WMDE\Fundraising\Frontend\DonationContext\Domain\Model\DonationTrackingInfo;
12
use WMDE\Fundraising\Frontend\DonationContext\Domain\Model\Donor;
13
use WMDE\Fundraising\Frontend\DonationContext\Domain\Model\DonorAddress;
14
use WMDE\Fundraising\Frontend\DonationContext\Domain\Model\DonorName;
15
use WMDE\Fundraising\Frontend\DonationContext\Domain\Repositories\DonationRepository;
16
use WMDE\Fundraising\Frontend\DonationContext\Domain\Repositories\GetDonationException;
17
use WMDE\Fundraising\Frontend\DonationContext\Domain\Repositories\StoreDonationException;
18
use WMDE\Fundraising\Frontend\DonationContext\Infrastructure\DonationConfirmationMailer;
19
use WMDE\Fundraising\Frontend\DonationContext\Infrastructure\DonationEventLogger;
20
use WMDE\Fundraising\Frontend\PaymentContext\Domain\Model\PayPalData;
21
use WMDE\Fundraising\Frontend\PaymentContext\Domain\Model\PayPalPayment;
22
23
/**
24
 * @license GNU GPL v2+
25
 * @author Kai Nissen < [email protected] >
26
 * @author Gabriel Birke < [email protected] >
27
 */
28
class HandlePayPalPaymentNotificationUseCase {
29
30
	private $repository;
31
	private $authorizationService;
32
	private $mailer;
33
	private $logger;
34
	private $donationEventLogger;
35
36
	public function __construct( DonationRepository $repository, DonationAuthorizer $authorizationService,
37
								 DonationConfirmationMailer $mailer, LoggerInterface $logger,
38
								 DonationEventLogger $donationEventLogger ) {
39
		$this->repository = $repository;
40
		$this->authorizationService = $authorizationService;
41
		$this->mailer = $mailer;
42
		$this->logger = $logger;
43
		$this->donationEventLogger = $donationEventLogger;
44
	}
45
46
	public function handleNotification( PayPalNotificationRequest $request ): bool {
47
		if ( !$this->requestCanBeHandled( $request ) ) {
48
			return false;
49
		}
50
51
		try {
52
			$donation = $this->repository->getDonationById( $request->getDonationId() );
53
		} catch ( GetDonationException $ex ) {
54
			return false;
55
		}
56
57
		if ( $donation === null ) {
58
			return $this->handleRequestWithoutDonation( $request );
59
		}
60
61
		if ( $donation->isBooked() && $request->isRecurringPaymentCompletion() ) {
62
			return $this->handleRequestWithoutDonation( $request );
63
		}
64
65
		return $this->handleRequestForDonation( $request, $donation );
66
	}
67
68
	private function requestCanBeHandled( PayPalNotificationRequest $request ): bool {
69
		if ( !$request->isSuccessfulPaymentNotification() ) {
70
			$this->logUnhandledStatus( $request );
71
			return false;
72
		}
73
74
		if ( $this->transactionIsSubscriptionRelatedButNotAPayment( $request ) ) {
75
			$this->logUnhandledNonPayment( $request );
76
			return false;
77
		}
78
		return true;
79
	}
80
81
	private function handleRequestWithoutDonation( PayPalNotificationRequest $request ): bool {
82
		$donation = $this->newDonationFromRequest( $request );
83
84
		try {
85
			$this->repository->storeDonation( $donation );
86
		} catch ( StoreDonationException $ex ) {
87
			return false;
88
		}
89
90
		$this->sendConfirmationEmailFor( $donation );
91
		$this->donationEventLogger->log( $donation->getId(), 'paypal_handler: booked' );
92
93
		return true;
94
	}
95
96
	private function handleRequestForDonation( PayPalNotificationRequest $request, Donation $donation ): bool {
97
		if ( !( $donation->getPayment()->getPaymentMethod() instanceof PayPalPayment ) ) {
98
			return false;
99
		}
100
101
		if ( !$this->authorizationService->systemCanModifyDonation( $request->getDonationId() ) ) {
102
			return false;
103
		}
104
		if ( $this->donationWasBookedWithDifferentTransactionId( $donation, $request ) ) {
105
			$childDonation = $this->createChildDonation( $donation, $request );
106
			return $childDonation !== null;
107
		}
108
109
		$donation->addPayPalData( $this->newPayPalDataFromRequest( $request ) );
110
111
		try {
112
			$donation->confirmBooked();
113
		} catch ( \RuntimeException $ex ) {
114
			return false;
115
		}
116
117
		try {
118
			$this->repository->storeDonation( $donation );
119
		}
120
		catch ( StoreDonationException $ex ) {
121
			return false;
122
		}
123
124
		$this->sendConfirmationEmailFor( $donation );
125
		$this->donationEventLogger->log( $donation->getId(), 'paypal_handler: booked' );
126
127
		return true;
128
	}
129
130
	private function logUnhandledStatus( PayPalNotificationRequest $request ) {
131
		$logContext = [
132
			'payment_status' => $request->getPaymentStatus(),
133
			'txn_id' => $request->getTransactionId()
134
		];
135
		$this->logger->info( 'Unhandled PayPal notification: ' . $request->getPaymentStatus(), $logContext );
136
	}
137
138
	private function logUnhandledNonPayment( PayPalNotificationRequest $request ) {
139
		$logContext = [
140
			'transaction_type' => $request->getTransactionType(),
141
			'txn_id' => $request->getTransactionId()
142
		];
143
		$this->logger->info( 'Unhandled PayPal subscription notification: ' . $request->getTransactionType(), $logContext );
144
	}
145
146
	private function sendConfirmationEmailFor( Donation $donation ) {
147
		if ( $donation->getDonor() !== null ) {
148
			try {
149
				$this->mailer->sendConfirmationMailFor( $donation );
150
			} catch ( \RuntimeException $ex ) {
151
				// no need to re-throw or return false, this is not a fatal error, only a minor inconvenience
152
			}
153
		}
154
	}
155
156
	private function transactionIsSubscriptionRelatedButNotAPayment( PayPalNotificationRequest $request ): bool {
157
		return $request->isForRecurringPayment() && !$request->isRecurringPaymentCompletion();
158
	}
159
160
	private function newPayPalDataFromRequest( PayPalNotificationRequest $request ): PayPalData {
161
		return ( new PayPalData() )
162
			->setPayerId( $request->getPayerId() )
163
			->setSubscriberId( $request->getSubscriberId() )
164
			->setPayerStatus( $request->getPayerStatus() )
165
			->setAddressStatus( $request->getPayerAddressStatus() )
166
			->setAmount( $request->getAmountGross() )
167
			->setCurrencyCode( $request->getCurrencyCode() )
168
			->setFee( $request->getTransactionFee() )
169
			->setSettleAmount( $request->getSettleAmount() )
170
			->setFirstName( $request->getPayerFirstName() )
171
			->setLastName( $request->getPayerLastName() )
172
			->setAddressName( $request->getPayerAddressName() )
173
			->setPaymentId( $request->getTransactionId() )
174
			->setPaymentType( $request->getPaymentType() )
175
			->setPaymentStatus( implode( '/', [ $request->getPaymentStatus(), $request->getTransactionType() ] ) )
176
			->setPaymentTimestamp( $request->getPaymentTimestamp() );
177
	}
178
179
	private function donationWasBookedWithDifferentTransactionId( Donation $donation,
180
																  PayPalNotificationRequest $request ): bool {
181
		/**
182
		 * @var PayPalPayment $payment
183
		 */
184
		$payment = $donation->getPaymentMethod();
185
186
		if ( !$donation->isBooked() ) {
187
			return false;
188
		}
189
190
		if ( $request->getTransactionId() === $payment->getPayPalData()->getPaymentId() ) {
191
			return false;
192
		}
193
194
		if ( $payment->getPayPalData()->hasChildPayment( $request->getTransactionId() ) ) {
195
			return false;
196
		}
197
198
		return true;
199
	}
200
201
	private function createChildDonation( Donation $donation, PayPalNotificationRequest $request ) {
202
		$childPaymentMethod = new PayPalPayment( $this->newPayPalDataFromRequest( $request ) );
203
		$payment = $donation->getPayment();
204
		$childDonation = new Donation(
205
			null,
206
			Donation::STATUS_EXTERNAL_BOOKED,
207
			$donation->getDonor(),
208
			new DonationPayment( $payment->getAmount(), $payment->getIntervalInMonths(), $childPaymentMethod ),
209
			$donation->getOptsIntoNewsletter(), $donation->getTrackingInfo()
210
		);
211
		try {
212
			$this->repository->storeDonation( $childDonation );
213
		} catch ( StoreDonationException $ex ) {
214
			return null;
215
		}
216
		/** @var \WMDE\Fundraising\Frontend\PaymentContext\Domain\Model\PayPalPayment $paymentMethod */
217
		$paymentMethod = $payment->getPaymentMethod();
218
		$paymentMethod->getPayPalData()->addChildPayment( $request->getTransactionId(), $childDonation->getId() );
219
		try {
220
			$this->repository->storeDonation( $donation );
221
		} catch ( StoreDonationException $ex ) {
222
			return null;
223
		}
224
		$this->logChildDonationCreatedEvent( $donation->getId(), $childDonation->getId() );
225
		return $childDonation;
226
	}
227
228
	private function logChildDonationCreatedEvent( $parentId, $childId ) {
229
		$this->donationEventLogger->log(
230
			$parentId,
231
			"paypal_handler: new transaction id to corresponding child donation: $childId"
232
		);
233
		$this->donationEventLogger->log(
234
			$childId,
235
			"paypal_handler: new transaction id to corresponding parent donation: $parentId"
236
		);
237
	}
238
239
	private function newDonorFromRequest( PayPalNotificationRequest $request ): Donor {
240
		return new Donor(
241
			$this->newPersonNameFromRequest( $request ),
242
			$this->newPhysicalAddressFromRequest( $request ),
243
			$request->getPayerEmail()
244
		);
245
	}
246
247
	private function newPersonNameFromRequest( PayPalNotificationRequest $request ): DonorName {
248
		$name = DonorName::newPrivatePersonName();
249
		$name->setFirstName( $request->getPayerFirstName() );
250
		$name->setLastName( $request->getPayerLastName() );
251
		$name->freeze();
252
		return $name;
253
	}
254
255
	private function newPhysicalAddressFromRequest( PayPalNotificationRequest $request ): DonorAddress {
256
		$address = new DonorAddress();
257
		$address->setStreetAddress( $request->getPayerAddressStreet() );
258
		$address->setCity( $request->getPayerAddressCity() );
259
		$address->setPostalCode( $request->getPayerAddressPostalCode() );
260
		$address->setCountryCode( $request->getPayerAddressCountryCode() );
261
		$address->freeze();
262
		return $address;
263
	}
264
265
	private function newDonationFromRequest( PayPalNotificationRequest $request ): Donation {
266
		$payment = new DonationPayment( $request->getAmountGross(), 0, new PayPalPayment() );
267
		$donation = new Donation(
268
			null,
269
			Donation::STATUS_EXTERNAL_BOOKED,
270
			$this->newDonorFromRequest( $request ),
271
			$payment,
272
			Donation::DOES_NOT_OPT_INTO_NEWSLETTER,
273
			new DonationTrackingInfo()
274
		);
275
		$donation->addPayPalData( $this->newPayPalDataFromRequest( $request ) );
276
		return $donation;
277
	}
278
}
279