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;
}
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;
}
}
