<?php
declare(strict_types=1);
namespace Lnb\Shopware6\FraudPrevention\Subscriber;
use Lnb\Shopware6\FraudPrevention\Checkout\FraudPreventionCheckoutContext;
use Lnb\Shopware6\FraudPrevention\Checkout\FraudPreventionCheckoutContextFactory;
use Lnb\Shopware6\FraudPrevention\Checkout\Validation\AddressIsPackstation\AddressIsPackstation;
use Lnb\Shopware6\FraudPrevention\Checkout\Validation\CustomerGroup\CustomerGroup;
use Lnb\Shopware6\FraudPrevention\Checkout\Validation\CustomerPhone\CustomerPhone;
use Lnb\Shopware6\FraudPrevention\Checkout\Validation\FraudContextValidation;
use Lnb\Shopware6\FraudPrevention\Checkout\Validation\LineItemCount\LineItemCount;
use Lnb\Shopware6\FraudPrevention\Checkout\Validation\OrderInPeriod\OrderInPeriod;
use Lnb\Shopware6\FraudPrevention\Checkout\Validation\QuantityValidator\LineItemQuantity;
use Lnb\Shopware6\FraudPrevention\Checkout\Validation\ZipCode\Zipcode;
use Lnb\Shopware6\FraudPrevention\Installer\StateInstaller;
use Shopware\Core\Checkout\Cart\Order\CartConvertedEvent;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\Validator\Constraint;
use Symfony\Component\Validator\ConstraintViolation;
use Symfony\Component\Validator\ConstraintViolationInterface;
use Symfony\Component\Validator\ConstraintViolationListInterface;
use Symfony\Component\Validator\Validator\ValidatorInterface;
class CartConvertedSubscriber implements EventSubscriberInterface
{
private FraudPreventionCheckoutContextFactory $checkoutStructFactory;
private ValidatorInterface $validator;
/**
* @var array<string>
*/
private array $logMessage = [];
public function __construct(
FraudPreventionCheckoutContextFactory $checkoutStructFactory,
ValidatorInterface $validator
) {
$this->checkoutStructFactory = $checkoutStructFactory;
$this->validator = $validator;
}
public static function getSubscribedEvents(): array
{
return [
CartConvertedEvent::class => 'onCartConvertedEvent',
];
}
public function onCartConvertedEvent(CartConvertedEvent $event): void
{
$this->validator->startContext();
$this->logMessage = [];
$checkoutFraudStruct = $this->checkoutStructFactory->createFromCartConvertedEvent($event);
$fraudContextValidation = new FraudContextValidation($checkoutFraudStruct);
if ($fraudContextValidation->hasFreePartnerMeetingRegistration()) {
$this->setOrderComplete($event, $checkoutFraudStruct);
return;
}
//check countries on black list
if ($fraudContextValidation->hasCountryOnBlacklist()) {
$this->logError('The country is on blacklist', 'EmailBlackList');
$this->setFraudPreventedState($event, $checkoutFraudStruct);
}
// check if transaction is open and payment has to be checked
if ($fraudContextValidation->isPaymentStateOpenAndHasToBeChecked()) {
$this->logError('The PaymentState is open and the PaymentMethod is configured to be checked', 'PaymentStateOpen');
$this->setFraudPreventedState($event, $checkoutFraudStruct);
}
//fraud prevention is disabled
if (! $checkoutFraudStruct->getConfigServiceAdapter()->isFraudPreventionActive()) {
$this->saveLog($event);
return;
}
$validationBeforePrice = $this->validator->validate(
$checkoutFraudStruct,
[
new CustomerGroup(),
new OrderInPeriod(),
new LineItemQuantity(),
new LineItemCount(),
]
);
// check if paymentMethod has not to be checked extensive
if (! $fraudContextValidation->hasPaymentMethodForExtensiveCheck()) {
$this->saveValidatorLog($validationBeforePrice, $event, $checkoutFraudStruct);
return;
}
$validationBeforePrice->addAll($this->validator->validate(
$checkoutFraudStruct,
new Zipcode()
));
$this->saveValidatorLog($validationBeforePrice, $event, $checkoutFraudStruct);
if ($checkoutFraudStruct->getPrice()->getTotalPrice() <= $checkoutFraudStruct->getConfigServiceAdapter()->getMiddleOrderPrice()) {
$this->saveLog($event);
return;
}
if ($checkoutFraudStruct->getPrice()->getTotalPrice() >= $checkoutFraudStruct->getConfigServiceAdapter()->getMaximalOrderPrice()) {
$this->logError('Total price is over maximal order price', 'TotalPriceOverMaximum');
$this->setFraudPreventedState($event, $checkoutFraudStruct);
$this->saveLog($event);
return;
}
if (! $fraudContextValidation->hasEmailOnBlacklist()) {
$this->saveLog($event);
return;
}
$this->logError('Email domain on blacklist', 'emailDomainOnBlacklist');
$validationAfterPrice = $this->validator->validate(
$checkoutFraudStruct,
[
new CustomerPhone(),
new AddressIsPackstation(),
]
);
$this->saveValidatorLog($validationAfterPrice, $event, $checkoutFraudStruct);
}
protected function setFraudPreventedState(CartConvertedEvent $event, FraudPreventionCheckoutContext $context): void
{
if (empty($this->logMessage)) {
throw new \RuntimeException('Order ist set to fraud without a reason');
}
$converted = $event->getConvertedCart();
$converted['stateId'] = strtolower(StateInstaller::FRAUD_PREVENTED_STATE_ID);
//todo check if a custom tag can be set to the order
//@todo add test for this
$tagId = $context->getFraudOrderTag(); //@todo get tag id from config
if (! empty($tagId)) {
$converted['tags'] = [
[
'id' => $tagId,
],
];
}
$event->setConvertedCart($converted);
}
protected function logError(string $message, string $key = null): void
{
if ($key === null) {
$this->logMessage[] = $message;
return;
}
$this->logMessage[$key] = $message;
}
protected function saveLog(CartConvertedEvent $event): void
{
if (empty($this->logMessage)) {
return;
}
$converted = $event->getConvertedCart();
$converted['customFields']['fraudLog'] = $this->logMessage;
$event->setConvertedCart($converted);
}
private function setOrderComplete(CartConvertedEvent $event, FraudPreventionCheckoutContext $checkoutFraudStruct): void
{
$converted = $event->getConvertedCart();
$openState = $checkoutFraudStruct->getOrderCompleteStateId();
$converted['stateId'] = strtolower($openState);
$event->setConvertedCart($converted);
}
private function saveValidatorLog(ConstraintViolationListInterface $violationList, CartConvertedEvent $event, FraudPreventionCheckoutContext $context): void
{
if ($violationList->count() > 0) {
foreach ($violationList as $error) {
$this->logError((string) $error->getMessage(), $this->getConstraintNameOfViolation($error));
}
$this->setFraudPreventedState($event, $context);
}
$this->saveLog($event);
}
private function getConstraintNameOfViolation(ConstraintViolationInterface $error): ?string
{
if ($error instanceof ConstraintViolation && $error->getConstraint() instanceof Constraint) {
return (new \ReflectionClass(get_class($error->getConstraint())))->getShortName();
}
return null;
}
}