commerce_signifyd-1.0.x-dev/src/Signifyd/Webhook.php
src/Signifyd/Webhook.php
<?php namespace Drupal\commerce_signifyd\Signifyd; use Drupal\commerce_signifyd\Entity\SignifydTeamInterface; use Drupal\commerce_signifyd\Event\SignifydEvents; use Drupal\commerce_signifyd\Event\SignifydWebhookEvent; use Drupal\Component\Serialization\Json; use Drupal\Core\Entity\EntityMalformedException; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpKernel\Exception\BadRequestHttpException; /** * {@inheritdoc} */ class Webhook extends SignifydAbstract implements WebhookInterface { /** * {@inheritdoc} */ public function events(Request $request, SignifydTeamInterface $signifyd_team) { $body = $request->getContent(); $topic = $request->headers->get('X-SIGNIFYD-TOPIC'); $hash = $request->headers->get('X-SIGNIFYD-SEC-HMAC-SHA256'); $valid = $this->validWebhookRequest($body, $hash, $topic, $signifyd_team); if ($this->logging) { $this->logger->notice(t('<code>@body</code></br><code>@topic</code></br><code>@hash</code>', [ '@body' => $body, '@topic' => $topic, '@hash' => $hash, ])); } // Each payload needs to be validated. if (!$valid) { throw new BadRequestHttpException('Validation failed'); } $case_storage = $this->entityTypeManager->getStorage('signifyd_case'); $signifyd_data = Json::decode($body); if (!isset($signifyd_data['caseId'])) { throw new BadRequestHttpException('Malformed request'); } /** @var \Drupal\commerce_signifyd\Entity\SignifydCaseInterface $signifyd_case */ $signifyd_case = $case_storage->load($signifyd_data['caseId']); // Determine if we create or update Signifyd Case. $entity_action = $signifyd_case ? self::SIGNIFYD_UPDATE : self::SIGNIFYD_CREATE; switch ($topic) { case self::SIGNIFYD_CASE_CREATION: case self::SIGNIFYD_CASE_RESCORE: case self::SIGNIFYD_CASE_REVIEW: // Either create or update existing Signifyd case. if ($entity_action === self::SIGNIFYD_CREATE) { $signifyd_case = $case_storage->create([ 'case_id' => $signifyd_data['caseId'], 'order_id' => $signifyd_data['orderId'], 'score' => $signifyd_data['score'] ?? 0, 'guarantee' => $signifyd_data['guaranteeDisposition'], 'investigation_id' => $signifyd_data['investigationId'], 'status' => $signifyd_data['status'], 'team_id' => $signifyd_team->id(), ]); } else { $signifyd_case->setScore($signifyd_data['score'] ?? 0); $signifyd_case->setGuarantee($signifyd_data['guaranteeDisposition']); $signifyd_case->setInvestigationId($signifyd_data['investigationId']); $signifyd_case->set('status', $signifyd_data['status']); $signifyd_case->setTeam($signifyd_team); } $signifyd_case->save(); $this->createWebhookLog($signifyd_case, $topic); break; case self::SIGNIFYD_DESCISION_MADE: // Either create or update existing Signifyd case. if ($entity_action === self::SIGNIFYD_CREATE) { $signifyd_case = $case_storage->create([ 'case_id' => $signifyd_data['caseId'], 'order_id' => $signifyd_data['customerCaseId'], 'score' => $signifyd_data['score'] ?? 0, 'decision' => $signifyd_data['checkpointAction'], 'team_id' => $signifyd_team->id(), ]); } else { $signifyd_case->setScore($signifyd_data['score']); $signifyd_case->setDecision($signifyd_data['checkpointAction']); $signifyd_case->setTeam($signifyd_team); } // When only decision made webhook is used, we don't get guarantee // value. Mapping only ACCEPT and REJECT. if (isset(self::SIGNIFYD_MAP_ACTION_TO_GUARANTEE[$signifyd_data['checkpointAction']])) { $signifyd_case->setGuarantee(self::SIGNIFYD_MAP_ACTION_TO_GUARANTEE[$signifyd_data['checkpointAction']]); } $signifyd_case->save(); $this->createWebhookLog($signifyd_case, $topic); break; default: // Do nothing out of box. // Non specified topics falls into this category: // claims/paid, claim/reviewed, decisions/*. if (!$signifyd_case && $topic !== 'cases/test') { throw new EntityMalformedException('Non-existing case'); } } // For test webhook call we don't have Signifyd case. if ($signifyd_case) { // Trigger event to react on Signifyd webhook updates. $event = new SignifydWebhookEvent($signifyd_case, $signifyd_data, $entity_action, $topic); $this->eventDispatcher->dispatch($event, SignifydEvents::SIGNIFYD_WEBHOOK); $order = $signifyd_case->getOrder(); $order_type = $order->bundle(); // Check if automatic workflow is enabled. if ($this->signifydSettings->get('order_types.' . $order_type . '.workflow')) { $declined_transition = $this->signifydSettings->get('order_types.' . $order_type . '.declined'); $approved_transition = $this->signifydSettings->get('order_types.' . $order_type . '.approved'); $decision_type = $this->signifydSettings->get('decision_type'); $move_to_declined = FALSE; $move_to_approved = FALSE; switch ($decision_type) { case 'score': $case_score = $signifyd_case->getScore(); // Only move if there is a score. if ($case_score > 0) { if ($signifyd_case->getScore() >= (int) $this->signifydSettings->get('score')) { $move_to_approved = TRUE; } else { $move_to_declined = TRUE; } } break; case 'decision': // Move unless decision is HOLD. if ($signifyd_case->getDecision() === 'ACCEPT') { $move_to_approved = TRUE; } elseif ($signifyd_case->getDecision() === 'REJECT') { $move_to_declined = TRUE; } break; // Guarantee logic. default: // Move unless guarantee is in review or pending. if ($signifyd_case->getGuarantee() === 'APPROVED') { $move_to_approved = TRUE; } elseif (in_array($signifyd_case->getGuarantee(), [ 'CANCELED', 'DECLINED', ])) { $move_to_declined = TRUE; } } if ($move_to_declined && $order->getState()->isTransitionAllowed($declined_transition)) { $order->getState()->applyTransitionById($declined_transition); $order->save(); } if ($move_to_approved && $order->getState()->isTransitionAllowed($approved_transition)) { $order->getState()->applyTransitionById($approved_transition); $order->save(); } } } $response = new Response(); $response->setStatusCode(200); return $response; } /** * Validate a webhook request. * * @param string $body * The request body. * @param string $hash * The hashed request. * @param string $topic * The topic. * @param \Drupal\commerce_signifyd\Entity\SignifydTeamInterface $signifyd_team * The Signifyd team. * * @return bool * Return true if validated. * * @see https://github.com/signifyd/php/blob/main/lib/Core/Api/WebhooksApi.php */ protected function validWebhookRequest($body, $hash, $topic, SignifydTeamInterface $signifyd_team) { if (empty($body) || empty($hash) || empty($topic)) { return FALSE; } $check = base64_encode( hash_hmac('sha256', $body, $signifyd_team->getApiKey(), TRUE) ); if ($check == $hash) { return TRUE; } else { if ($topic == "cases/test") { // In the case that this is a webhook test, // the encoding ABCDE is allowed. $check = base64_encode( hash_hmac('sha256', $body, 'ABCDE', TRUE) ); if ($check == $hash) { return TRUE; } } } return FALSE; } }