From 5dc54d197b63ba0ec5f17c48bd18cac52b31f62a Mon Sep 17 00:00:00 2001 From: Glauber Silva Date: Thu, 15 Aug 2024 09:05:42 -0300 Subject: [PATCH] Refactor: prevent subscription renewals with gateway transaction IDs already used previously (#7449) --- .../Repositories/DonationRepository.php | 8 ++++ .../SubscriptionRenewalDonationCreated.php | 43 +++++++++++++++++ ...SubscriptionRenewalDonationCreatedTest.php | 48 +++++++++++++++++++ 3 files changed, 99 insertions(+) diff --git a/src/Donations/Repositories/DonationRepository.php b/src/Donations/Repositories/DonationRepository.php index ec47cf7b81..5042d9c9be 100644 --- a/src/Donations/Repositories/DonationRepository.php +++ b/src/Donations/Repositories/DonationRepository.php @@ -75,6 +75,14 @@ public function getByGatewayTransactionId($gatewayTransactionId) return $this->queryByGatewayTransactionId($gatewayTransactionId)->get(); } + /** + * @unreleased + */ + public function getTotalDonationCountByGatewayTransactionId($gatewayTransactionId): int + { + return $this->queryByGatewayTransactionId($gatewayTransactionId)->count(); + } + /** * @since 2.21.0 */ diff --git a/src/Framework/PaymentGateways/Webhooks/EventHandlers/SubscriptionRenewalDonationCreated.php b/src/Framework/PaymentGateways/Webhooks/EventHandlers/SubscriptionRenewalDonationCreated.php index 180b3e575f..94888bf3ff 100644 --- a/src/Framework/PaymentGateways/Webhooks/EventHandlers/SubscriptionRenewalDonationCreated.php +++ b/src/Framework/PaymentGateways/Webhooks/EventHandlers/SubscriptionRenewalDonationCreated.php @@ -15,6 +15,7 @@ class SubscriptionRenewalDonationCreated { /** + * @unreleased Add log messages and a defensive approach to prevent duplicated renewals * @since 3.6.0 */ public function __invoke( @@ -25,6 +26,48 @@ public function __invoke( $subscription = give()->subscriptions->getByGatewaySubscriptionId($gatewaySubscriptionId); if ( ! $subscription) { + PaymentGatewayLog::error( + sprintf('The renewal was not created for the gateway transaction ID %s because no subscription with the gateway subscription %s was found.', + $gatewayTransactionId, $gatewaySubscriptionId), + [ + 'Gateway Subscription ID' => $gatewaySubscriptionId, + 'Gateway Transaction ID' => $gatewayTransactionId, + 'Message' => $message, + ] + ); + + return; + } + + if ($subscription->initialDonation()->gatewayTransactionId === $gatewayTransactionId) { + PaymentGatewayLog::error( + sprintf('The renewal was not created for the gateway transaction ID %s because the initial donation of the subscription %s is already using the informed gateway transaction ID %s.', + $gatewayTransactionId, $subscription->id, $gatewaySubscriptionId), + [ + 'Gateway Subscription ID' => $gatewaySubscriptionId, + 'Gateway Transaction ID' => $gatewayTransactionId, + 'Message' => $message, + 'Subscription' => $subscription->toArray(), + ] + ); + + return; + } + + $donation = give()->donations->getByGatewayTransactionId($gatewayTransactionId); + + if ($donation) { + PaymentGatewayLog::error( + sprintf('The renewal was not created for the gateway transaction ID %s because the donation %s is already using the informed gateway transaction ID %s.', + $gatewayTransactionId, $donation->id, $gatewaySubscriptionId), + [ + 'Gateway Subscription ID' => $gatewaySubscriptionId, + 'Gateway Transaction ID' => $gatewayTransactionId, + 'Message' => $message, + 'Donation' => $donation->toArray(), + ] + ); + return; } diff --git a/tests/Unit/Framework/PaymentGateways/Webhooks/EventHandlers/SubscriptionRenewalDonationCreatedTest.php b/tests/Unit/Framework/PaymentGateways/Webhooks/EventHandlers/SubscriptionRenewalDonationCreatedTest.php index b0de74412a..455bbac405 100644 --- a/tests/Unit/Framework/PaymentGateways/Webhooks/EventHandlers/SubscriptionRenewalDonationCreatedTest.php +++ b/tests/Unit/Framework/PaymentGateways/Webhooks/EventHandlers/SubscriptionRenewalDonationCreatedTest.php @@ -44,4 +44,52 @@ public function testShouldCreateRenewalDonation() $this->assertEquals($subscription->id, $renewalDonation->subscriptionId); } + + /** + * @unreleased + * + * @throws Exception + */ + public function testShouldNotCreateRenewalDonationWithFirstGatewayTransactionId() + { + $subscription = Subscription::factory()->createWithDonation(); + $donation = $subscription->initialDonation(); + + $firstGatewayTransactionId = 'first-gateway-transaction-id'; + + $donation->status = DonationStatus::COMPLETE(); + $donation->gatewayTransactionId = $firstGatewayTransactionId; + $donation->save(); + + give(SubscriptionRenewalDonationCreated::class)($subscription->gatewaySubscriptionId, + $firstGatewayTransactionId); + + $totalDonations = give()->donations->getTotalDonationCountByGatewayTransactionId($firstGatewayTransactionId); + + $this->assertEquals(1, $totalDonations); + } + + /** + * @unreleased + * + * @throws Exception + */ + public function testShouldNotCreateRenewalDonationWithDuplicatedGatewayTransactionId() + { + $subscription = Subscription::factory()->createWithDonation(); + + $duplicatedGatewayTransactionId = 'duplicated-gateway-transaction-id'; + + // #1 Renewal Donation + give(SubscriptionRenewalDonationCreated::class)($subscription->gatewaySubscriptionId, + $duplicatedGatewayTransactionId); + + // #2 Renewal Donation - This one should not be created + give(SubscriptionRenewalDonationCreated::class)($subscription->gatewaySubscriptionId, + $duplicatedGatewayTransactionId); + + $totalDonations = give()->donations->getTotalDonationCountByGatewayTransactionId($duplicatedGatewayTransactionId); + + $this->assertEquals(1, $totalDonations); + } }