Skip to content

Commit b83209b

Browse files
authored
[ECP-9818] Remove the thrown exception in the case of duplicate webhooks (#3089)
1 parent 3814023 commit b83209b

File tree

6 files changed

+69
-89
lines changed

6 files changed

+69
-89
lines changed

Controller/Webhook/Index.php

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -82,17 +82,24 @@ public function execute(): ResultInterface
8282
$notifications = $acceptor->getNotifications($rawPayload);
8383

8484
foreach ($notifications as $notification) {
85-
$notification = $this->adyenNotificationRepository->save($notification);
86-
$this->adyenLogger->addAdyenResult(sprintf("Notification %s is accepted", $notification->getId()));
85+
if ($notification->isDuplicate()) {
86+
$this->adyenLogger->addAdyenResult(sprintf(
87+
"Duplicate notification with pspReference %s has been skipped.",
88+
$notification->getPspReference()
89+
));
90+
} else {
91+
$notification = $this->adyenNotificationRepository->save($notification);
92+
$this->adyenLogger->addAdyenResult(
93+
sprintf("Notification %s is accepted", $notification->getId())
94+
);
95+
}
8796
}
8897

8998
return $this->prepareResponse('[accepted]', 200);
9099
} catch (AuthenticationException $e) {
91100
return $this->prepareResponse(__('Unauthorized'), 401);
92101
} catch (InvalidDataException $e) {
93102
return $this->prepareResponse(__('The request does not contain a valid webhook!'), 400);
94-
} catch (AlreadyExistsException $e) {
95-
return $this->prepareResponse(__('Webhook already exists!'), 400);
96103
} catch (Exception $e) {
97104
$this->adyenLogger->addAdyenNotification($e->getMessage(), $rawPayload ?? []);
98105

Model/Webhook/StandardWebhookAcceptor.php

Lines changed: 7 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -24,8 +24,8 @@
2424
use Adyen\Webhook\Exception\InvalidDataException;
2525
use Adyen\Webhook\Receiver\NotificationReceiver;
2626
use Adyen\Webhook\Receiver\HmacSignature;
27-
use Magento\Framework\Exception\AlreadyExistsException;
2827
use Magento\Framework\Serialize\SerializerInterface;
28+
use Throwable;
2929

3030
class StandardWebhookAcceptor implements WebhookAcceptorInterface
3131
{
@@ -38,7 +38,7 @@ class StandardWebhookAcceptor implements WebhookAcceptorInterface
3838
* @param HmacSignature $hmacSignature
3939
* @param SerializerInterface $serializer
4040
* @param OrderHelper $orderHelper
41-
*/
41+
*/
4242
public function __construct(
4343
private readonly NotificationFactory $notificationFactory,
4444
private readonly AdyenLogger $adyenLogger,
@@ -53,7 +53,7 @@ public function __construct(
5353
/**
5454
* @throws AuthenticationException
5555
* @throws InvalidDataException
56-
* @throws AlreadyExistsException|HMACKeyValidationException
56+
* @throws HMACKeyValidationException
5757
*/
5858
public function getNotifications(array $payload): array
5959
{
@@ -76,14 +76,13 @@ public function getNotifications(array $payload): array
7676
try {
7777
$order = $this->orderHelper->getOrderByIncrementId($merchantReference);
7878
$storeId = $order?->getStoreId();
79-
} catch (\Throwable $e) {
79+
} catch (Throwable $e) {
8080
$this->adyenLogger->addAdyenNotification(
8181
sprintf('Could not load order for reference %s: %s', $merchantReference, $e->getMessage()),
8282
$payload
8383
);
8484
}
8585

86-
8786
$this->validate($item, $isLive, $storeId);
8887

8988
$notifications[] = $this->toNotification($item, $isLive);
@@ -130,7 +129,9 @@ private function validate(array $item, string $isLiveMode, ?int $storeId): void
130129
}
131130

132131
/**
133-
* @throws AlreadyExistsException
132+
* @param array $payload
133+
* @param string $isLive
134+
* @return Notification
134135
*/
135136
private function toNotification(array $payload, string $isLive): Notification
136137
{
@@ -183,10 +184,6 @@ private function toNotification(array $payload, string $isLive): Notification
183184

184185
$notification->setLive($isLive);
185186

186-
if ($notification->isDuplicate()) {
187-
throw new AlreadyExistsException(__('Webhook already exists!'));
188-
}
189-
190187
return $notification;
191188
}
192189
}

Model/Webhook/TokenWebhookAcceptor.php

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,6 @@
2323
use Adyen\Webhook\Exception\InvalidDataException;
2424
use Adyen\Webhook\Receiver\NotificationReceiver;
2525
use Magento\Framework\App\Request\Http;
26-
use Magento\Framework\Exception\AlreadyExistsException;
2726
use Magento\Framework\Serialize\SerializerInterface;
2827
use Magento\Sales\Model\Order;
2928

@@ -65,7 +64,6 @@ public function __construct(
6564
) { }
6665

6766
/**
68-
* @throws AlreadyExistsException
6967
* @throws InvalidDataException
7068
* @throws AuthenticationException
7169
*/
@@ -149,7 +147,9 @@ private function validate(array $payload, string $isLive): void
149147
}
150148

151149
/**
152-
* @throws AlreadyExistsException
150+
* @param array $payload
151+
* @param string $isLive
152+
* @return Notification
153153
*/
154154
private function toNotification(array $payload, string $isLive): Notification
155155
{
@@ -178,14 +178,14 @@ private function toNotification(array $payload, string $isLive): Notification
178178
$notification->setCreatedAt($formattedDate);
179179
$notification->setUpdatedAt($formattedDate);
180180

181-
// 💡 Add duplicate check here
182-
if ($notification->isDuplicate()) {
183-
throw new AlreadyExistsException(__('Webhook already exists!'));
184-
}
185-
186181
return $notification;
187182
}
188183

184+
/**
185+
* @param array $array
186+
* @param array $path
187+
* @return mixed
188+
*/
189189
private function getNestedValue(array $array, array $path): mixed
190190
{
191191
foreach ($path as $key) {

Test/Unit/Controller/Webhook/IndexTest.php

Lines changed: 43 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -143,6 +143,49 @@ public function testExecuteProcessesValidWebhook($payload, $eventType): void
143143
$this->assertInstanceOf(ResultInterface::class, $this->controller->execute());
144144
}
145145

146+
/**
147+
* @dataProvider dataProviderProcessValidWebhook
148+
*
149+
* @param $payload
150+
* @param $eventType
151+
* @return void
152+
* @throws Exception
153+
* @throws NotFoundException
154+
*/
155+
public function testExecuteProcessesDuplicateWebhook($payload, $eventType): void
156+
{
157+
$_SERVER['PHP_AUTH_USER'] = 'user';
158+
$_SERVER['PHP_AUTH_PW'] = 'pass';
159+
160+
$this->configHelperMock->method('getNotificationsUsername')->willReturn('user');
161+
$this->configHelperMock->method('getNotificationsPassword')->willReturn('pass');
162+
163+
$notification = $this->createMock(Notification::class);
164+
$notification->method('getPspreference')->willReturn('ABC12345678XYZ');
165+
$notification->method('isDuplicate')->willReturn(true);
166+
167+
$this->adyenNotificationRepositoryMock->expects($this->never())->method('save');
168+
169+
$acceptorMock = $this->createMock(WebhookAcceptorInterface::class);
170+
$acceptorMock->method('getNotifications')->willReturn([$notification]);
171+
172+
$this->requestMock->method('getContent')->willReturn(json_encode($payload));
173+
$this->webhookAcceptorFactoryMock->method('getAcceptor')
174+
->with($eventType)
175+
->willReturn($acceptorMock);
176+
177+
$this->adyenLoggerMock->expects($this->once())
178+
->method('addAdyenResult')
179+
->with('Duplicate notification with pspReference ABC12345678XYZ has been skipped.');
180+
181+
$this->webhookHelperMock->method('isIpValid')->willReturn(true);
182+
183+
$this->resultMock->expects($this->once())->method('setStatusHeader')->with(200);
184+
$this->resultMock->expects($this->once())->method('setContents')->with('[accepted]');
185+
186+
$this->assertInstanceOf(ResultInterface::class, $this->controller->execute());
187+
}
188+
146189
public function testWebhookUnidentifiedEventType(): void
147190
{
148191
$_SERVER['PHP_AUTH_USER'] = 'user';
@@ -238,26 +281,4 @@ public function testExecuteOnGenericError(): void
238281
->with('An error occurred while handling this webhook!');
239282
$this->assertInstanceOf(ResultInterface::class, $this->controller->execute());
240283
}
241-
242-
public function testExecuteOnDuplicateWebhook(): void
243-
{
244-
$_SERVER['PHP_AUTH_USER'] = 'user';
245-
$_SERVER['PHP_AUTH_PW'] = 'pass';
246-
247-
$this->configHelperMock->method('getNotificationsUsername')->willReturn('user');
248-
$this->configHelperMock->method('getNotificationsPassword')->willReturn('pass');
249-
$this->webhookHelperMock->method('isIpValid')->willReturn(true);
250-
251-
$payload = ['type' => 'token.created'];
252-
$this->requestMock->method('getContent')->willReturn(json_encode($payload));
253-
254-
$acceptorMock = $this->createMock(TokenWebhookAcceptor::class);
255-
$acceptorMock->method('getNotifications')->willThrowException(new AlreadyExistsException());
256-
$this->webhookAcceptorFactoryMock->method('getAcceptor')->willReturn($acceptorMock);
257-
258-
$this->resultMock->expects($this->once())->method('setStatusHeader')->with(400);
259-
$this->resultMock->expects($this->once())->method('setContents')
260-
->with('Webhook already exists!');
261-
$this->assertInstanceOf(ResultInterface::class, $this->controller->execute());
262-
}
263284
}

Test/Unit/Model/Webhook/StandardWebhookAcceptorTest.php

Lines changed: 0 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -255,35 +255,6 @@ public function testValidateThrowsAuthenticationExceptionWhenHmacInvalid(): void
255255
$this->acceptor->getNotifications($payload);
256256
}
257257

258-
/**
259-
* Duplicate notification -> AlreadyExistsException.
260-
*/
261-
public function testToNotificationThrowsAlreadyExistsExceptionOnDuplicate(): void
262-
{
263-
$this->expectException(AlreadyExistsException::class);
264-
265-
$payload = $this->getValidPayload();
266-
$item = $payload['notificationItems'][0]['NotificationRequestItem'];
267-
268-
// Env OK & merchant OK
269-
$this->orderHelperMock->method('getOrderByIncrementId')->willReturn(null);
270-
$this->configHelperMock->method('isDemoMode')->with(null)->willReturn(true);
271-
$this->notificationReceiverMock->method('validateNotificationMode')->with('false', true)->willReturn(true);
272-
$this->webhookHelperMock->method('isMerchantAccountValid')->with('TestMerchant', $item, 'webhook', null)->willReturn(true);
273-
274-
// HMAC check disabled (no key) to keep test focused
275-
$this->configHelperMock->method('getNotificationsHmacKey')->willReturn(null);
276-
$this->hmacSignatureMock->expects($this->never())->method('isHmacSupportedEventCode');
277-
$this->notificationReceiverMock->expects($this->never())->method('validateHmac');
278-
279-
// Duplicate
280-
$notification = $this->createMock(Notification::class);
281-
$notification->method('isDuplicate')->willReturn(true);
282-
$this->notificationFactoryMock->method('create')->willReturn($notification);
283-
284-
$this->acceptor->getNotifications($payload);
285-
}
286-
287258
/**
288259
* If HMAC key missing or event not supported, HMAC is not validated.
289260
* Here we simulate "event not supported".

Test/Unit/Model/Webhook/TokenWebhookAcceptorTest.php

Lines changed: 0 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -266,22 +266,6 @@ public function testValidateThrowsAuthenticationExceptionWithInvalidHmacSignatur
266266
$this->acceptor->getNotifications($payload);
267267
}
268268

269-
public function testToNotificationThrowsExceptionOnDuplicate(): void
270-
{
271-
$this->expectException(AlreadyExistsException::class);
272-
273-
$payload = $this->getValidPayload();
274-
275-
$notification = $this->createMock(Notification::class);
276-
$notification->method('isDuplicate')->willReturn(true);
277-
$this->notificationFactoryMock->method('create')->willReturn($notification);
278-
279-
$this->configHelperMock->method('isDemoMode')->willReturn(true);
280-
$this->webhookHelperMock->method('isMerchantAccountValid')->willReturn(true);
281-
282-
$this->acceptor->getNotifications($payload);
283-
}
284-
285269
public function testLogsAndContinuesWhenPaymentLoadThrows(): void
286270
{
287271
$payload = $this->getValidPayload();

0 commit comments

Comments
 (0)