<?php declare(strict_types=1);
namespace AbmAdjustments\Subscriber;
use Doctrine\DBAL\Connection;
use Psr\Cache\InvalidArgumentException;
use Shopware\Core\Checkout\Cart\AbstractCartPersister;
use Shopware\Core\Checkout\Cart\Address\Error\AddressValidationError;
use Shopware\Core\Checkout\Cart\Address\Error\ProfileSalutationMissingError;
use Shopware\Core\Checkout\Cart\Cart;
use Shopware\Core\Checkout\Cart\CartPersister;
use Shopware\Core\Checkout\Cart\Event\AfterLineItemAddedEvent;
use Shopware\Core\Checkout\Cart\Event\BeforeLineItemAddedEvent;
use Shopware\Core\Checkout\Cart\Event\CartChangedEvent;
use Shopware\Core\Checkout\Cart\Event\CartCreatedEvent;
use Shopware\Core\Checkout\Cart\Event\CartVerifyPersistEvent;
use Shopware\Core\Checkout\Cart\LineItem\LineItem;
use Shopware\Core\Checkout\Cart\Price\Struct\QuantityPriceDefinition;
use Shopware\Core\Checkout\Cart\SalesChannel\CartService;
use Shopware\Core\Checkout\Cart\Tax\Struct\CalculatedTax;
use Shopware\Core\Checkout\Cart\Tax\Struct\CalculatedTaxCollection;
use Shopware\Core\Content\Product\Events\ProductListingCriteriaEvent;
use Shopware\Core\Content\Product\Events\ProductSuggestCriteriaEvent;
use Shopware\Core\Content\Product\ProductEvents;
use Shopware\Core\Content\Product\SalesChannel\SalesChannelProductEntity;
use Shopware\Core\Framework\Context;
use Shopware\Core\Framework\DataAbstractionLayer\EntityRepositoryInterface;
use Shopware\Core\Framework\DataAbstractionLayer\Event\EntityLoadedEvent;
use Shopware\Core\Framework\DataAbstractionLayer\Search\Criteria;
use Shopware\Core\Framework\DataAbstractionLayer\Search\Filter\EqualsFilter;
use Shopware\Core\Framework\DataAbstractionLayer\Search\Filter\MultiFilter;
use Shopware\Core\Framework\DataAbstractionLayer\Search\Filter\NotFilter;
use Shopware\Core\Framework\Event\DataMappingEvent;
use Shopware\Core\Framework\Validation\DataBag\RequestDataBag;
use Shopware\Core\System\SalesChannel\Entity\SalesChannelEntityLoadedEvent;
use Shopware\Core\System\SystemConfig\Event\SystemConfigChangedEvent;
use Shopware\Storefront\Framework\Routing\RequestTransformer;
use Shopware\Core\System\SalesChannel\Context\SalesChannelContextFactory;
use Shopware\Core\System\SalesChannel\SalesChannelContext;
use Shopware\Core\System\SystemConfig\SystemConfigService;
use Shopware\Core\System\Tax\Aggregate\TaxRule\TaxRuleEntity;
use Shopware\Core\System\Tax\TaxEntity;
use Shopware\Storefront\Event\StorefrontRenderEvent;
use Shopware\Storefront\Page\Checkout\Cart\CheckoutCartPageLoadedEvent;
use Shopware\Storefront\Page\Checkout\Confirm\CheckoutConfirmPageLoadedEvent;
use Shopware\Storefront\Page\PageLoadedEvent;
use Shopware\Storefront\Page\Product\ProductPageLoadedEvent;
use Shopware\Storefront\Page\Search\SearchPageLoadedEvent;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\HttpFoundation\RedirectResponse;
use Symfony\Component\HttpFoundation\RequestStack;
use Symfony\Component\HttpFoundation\Session\SessionInterface;
use Symfony\Component\HttpKernel\Event\ControllerEvent;
use Symfony\Component\HttpKernel\Event\KernelEvent;
use Symfony\Component\HttpKernel\Event\RequestEvent;
use Symfony\Component\HttpKernel\Event\ResponseEvent;
use Symfony\Component\HttpKernel\Event\TerminateEvent;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
use Symfony\Component\HttpKernel\KernelEvents;
use Symfony\Config\Framework\UidConfig;
use Symfony\Contracts\Cache\CacheInterface;
use Symfony\Contracts\Cache\ItemInterface;
use Symfony\Component\Cache\Adapter\FilesystemAdapter;
use Shopware\Core\System\SalesChannel\Context\SalesChannelContextService;
use Shopware\Core\Framework\Util\StringHelper;
use Shopware\Core\Checkout\Order\Aggregate\OrderLineItem\OrderLineItemEntity;
use Shopware\Core\Checkout\Order\OrderEntity;
use Shopware\Core\Framework\Uuid\Uuid;
use Shopware\Core\Checkout\Cart\Tax\Struct\TaxRuleCollection;
use Symfony\Component\HttpKernel\Event\ExceptionEvent;
use Symfony\Component\ErrorHandler\Exception\FlattenException;
use Shopware\Storefront\Controller\StorefrontController;
use Shopware\Core\Content\Product\SalesChannel\Price\ProductPriceCalculator;
use Shopware\Core\Framework\DataAbstractionLayer\Search\Filter\EqualsAnyFilter;
use Shopware\Core\Framework\Struct\ArrayStruct;
/**
* @property SystemConfigService systemConfigService
*/
// extends StorefrontController
class FrontendSubscriber extends StorefrontController implements EventSubscriberInterface
{
const ALLOWED_DEMO_IPS = 'AkkusysTools.config.allowedDemoIps';
const TAX_DEMO_MODE = 'AkkusysTools.config.demoMode';
private $systemConfigService;
private $requestStack;
private $productRepository;
private $customFieldRepository;
private $connection;
private $cache;
private $cacheStage;
const ALL_ZERO_TAX_GROSS_NET_STG = 'all-zero-tax-gross-net-stg';
const ALL_ZERO_TAX_GROSS_NET_LIVE = 'all-zero-tax-gross-net-live';
private SalesChannelContextService $salesChannelContextService;
private CartService $cartService;
/**
* @param SystemConfigService $systemConfigService
* @param RequestStack $requestStack
* @param ContainerInterface $container
*/
public function __construct(
SystemConfigService $systemConfigService,
RequestStack $requestStack,
ContainerInterface $container,
EntityRepositoryInterface $productRepository,
EntityRepositoryInterface $customFieldRepository,
Connection $connection,
SalesChannelContextService $salesChannelContextService,
CartService $cartService
)
{
$this->systemConfigService = $systemConfigService;
$this->requestStack = $requestStack;
$this->container = $container;
$this->productRepository = $productRepository;
$this->customFieldRepository = $customFieldRepository;
$this->connection = $connection;
$this->cacheStage = new FilesystemAdapter();
$this->cache = new FilesystemAdapter();
$this->salesChannelContextService = $salesChannelContextService;
$this->cartService = $cartService;
}
/**
* @return array
*/
public static function getSubscribedEvents()
{
return [
ProductEvents::PRODUCT_LOADED_EVENT => "onProductLoaded",
CartChangedEvent::class => 'onCartChanged',
CartVerifyPersistEvent::class => 'onCartVerifyPersist',
SystemConfigChangedEvent::class => 'onConfigChanged',
'sales_channel.product.loaded' => 'onSalesChannelProductLoaded',
];
}
/**
* @param SalesChannelEntityLoadedEvent $event
*/
public function onSalesChannelProductLoaded(SalesChannelEntityLoadedEvent $event): void
{
foreach ($event->getEntities() as $productEntity) {
/* @var \Shopware\Core\Content\Product\SalesChannel\SalesChannelProductEntity $productEntity */
$bZeroTax = $productEntity->getTranslated()['customFields']['taxrate'] ?? 1;
if (!empty($bZeroTax)) {
continue;
}
$oReferencePrice = @$productEntity->getCalculatedPrice()->getReferencePrice() ?? null;
if(is_null($oReferencePrice)) {
continue;
}
$fReferencePrice = $oReferencePrice->getPrice();
$fCalculatedUnit = null;
if (!empty($productEntity->getCalculatedPrice()->getReferencePrice()->getPurchaseUnit())) {
$fCalculatedUnit = $productEntity->getCheapestPrice()->getPrice()->first()->getNet() / $productEntity->getCalculatedPrice()->getReferencePrice()->getPurchaseUnit();
}
if (!empty($fReferencePrice)) {
$customFields = $productEntity->getCustomFields();
$customFields['netReferencePrice'] = (int) ( ($fCalculatedUnit / (1.00)) * 100) / 100 ?? (int) ( ($productEntity->getCalculatedPrice()->getReferencePrice()->getPrice() / (1.19)) * 100) / 100;
$productEntity->setCustomFields($customFields);
}
}
}
/**
* @param SystemConfigChangedEvent $event
*/
public function onConfigChanged(SystemConfigChangedEvent $event)
{
if ($event->getKey() === 'zeroTaxMode') {
$currentRequest = $this->requestStack->getCurrentRequest();
$oCurrentSession = $currentRequest->getSession();
$oCurrentSession->clear();
}
}
/**
* @param CartChangedEvent $event
*/
public function onCartChanged(CartChangedEvent $event): void
{
$oCart = $event->getCart();
$context = $event->getContext();
$currentRequest = $this->requestStack->getCurrentRequest();
if ($currentRequest == null) {
return;
}
$oCurrentSession = $currentRequest->getSession();
$sRoute = $currentRequest->attributes->get('_route');
$oCustomer = $context->getCustomer();
$bLoggedIn = !is_null($oCustomer);
$bZeroTaxConsent = $oCurrentSession->get('zero_tax_consent');
$bAcceptDiscount = !empty($oCurrentSession->get('zero_tax_waiver'));
$bGroupPrices = ($bZeroTaxConsent && $bAcceptDiscount && $bLoggedIn) || !$bLoggedIn;
$groupedTotalAmounts = [];
foreach ($oCart->getLineItems() as $lineItem) {
// if ($this->isZeroTaxRate($lineItem) && $bGroupPrices && !$this->systemConfigService->get(self::FORCE_BRUT_TAX_SETTING)) {
if ($this->isZeroTaxRate($lineItem) && $bGroupPrices) {
$taxBase = 0;
} else {
$taxBase = $lineItem->getPrice()->getTaxRules()->first()->getTaxRate();
}
if (!isset($groupedTotalAmounts[$taxBase])) {
$groupedTotalAmounts[$taxBase] = 0;
}
$groupedTotalAmounts[$taxBase] += $lineItem->getPrice()->getTotalPrice();
}
if (!empty($groupedTotalAmounts)) {
$aCartExtension = $oCart->getExtensions();
$aCartExtension['groupedTaxes'] = $groupedTotalAmounts;
$oCart->setExtensions($aCartExtension);
$this->cartService->recalculate($oCart, $context);
if (!empty($groupedTotalAmounts[0]) && $sRoute !== 'frontend.checkout.confirm.page') {
$oCurrentSession->set('zero_tax_consent', true);
}
}
}
/**
* @param CartVerifyPersistEvent $event
*/
public function onCartVerifyPersist(CartVerifyPersistEvent $event): void
{
$oCart = $event->getCart();
$salesChannelContext = $event->getSalesChannelContext();
$currentRequest = $this->requestStack->getCurrentRequest();
if ($currentRequest == null) {
return;
}
$oCurrentSession = $currentRequest->getSession();
$sRoute = $currentRequest->attributes->get('_route');
$oCustomer = $salesChannelContext->getCustomer();
$bLoggedIn = !is_null($oCustomer);
$bZeroTaxConsent = $oCurrentSession->get('zero_tax_consent');
$bAcceptDiscount = empty($oCurrentSession->get('zero_tax_waiver'));
$bGroupPrices = ($bZeroTaxConsent && $bAcceptDiscount && $bLoggedIn) || !$bLoggedIn;
$groupedTotalAmounts = [];
foreach ($oCart->getLineItems() as $lineItem) {
if ($this->isZeroTaxRate($lineItem) && $bGroupPrices && $bAcceptDiscount) {
$taxBase = 0;
} else {
$taxBase = $lineItem->getPrice()->getTaxRules()->first()->getTaxRate();
}
if (!isset($groupedTotalAmounts[$taxBase])) {
$groupedTotalAmounts[$taxBase] = 0;
}
$groupedTotalAmounts[$taxBase] += $lineItem->getPrice()->getTotalPrice();
}
if (!empty($groupedTotalAmounts)) {
$aCartExtension = $oCart->getExtensions();
$aCartExtension['groupedTaxes'] = $groupedTotalAmounts;
$oCart->setExtensions($aCartExtension);
if (!empty($groupedTotalAmounts[0]) && $sRoute !== 'frontend.checkout.confirm.page') {
$oCurrentSession->set('zero_tax_consent', true);
}
}
}
/**
* @param LineItem $lineItem
* @return bool
*/
private function isZeroTaxRate(LineItem $lineItem): bool
{
return isset($lineItem->getPayload()['customFields']['taxrate']) && $lineItem->getPayload()['customFields']['taxrate'] === "0";
}
/**
* @param EntityLoadedEvent $event
* @throws InvalidArgumentException
*/
public function onProductLoaded(EntityLoadedEvent $event): void
{
if(php_sapi_name() === 'cli') {
return;
}
$currentRequest = $this->requestStack->getCurrentRequest();
if ($currentRequest == null) {
return;
}
$sRoute = $currentRequest->attributes->get('_route');
$oCurrentSession = $currentRequest->getSession();
$aAllowedDemoIps = [];
if (!empty($this->systemConfigService->get(self::ALLOWED_DEMO_IPS))) {
try {
$aAllowedDemoIps = explode(',', $this->systemConfigService->get(self::ALLOWED_DEMO_IPS)) ?? null;
} catch (\Exception $e) {
$aAllowedDemoIps = [];
}
}
$isDemosMode= $this->systemConfigService->get(self::TAX_DEMO_MODE) ?? null;
$bApplyZeroTaxFeature = !$isDemosMode ||
( $isDemosMode &&
(
in_array($_SERVER['REMOTE_ADDR'], $aAllowedDemoIps ) ||
in_array(@$_SERVER['HTTP_X_FORWARDED_FOR'], $aAllowedDemoIps )
)
);
if ($bApplyZeroTaxFeature) {
$bFirstZeroTaxHit = false;
/** @var SalesChannelProductEntity $productEntity */
foreach ($event->getEntities() as $productEntity) {
$customFields = $productEntity->getCustomFields();
$bZeroTax = $productEntity->getTranslated()['customFields']['taxrate'] ?? 1;
if (empty($bZeroTax)) {
if (!empty($productEntity->getPrices()->first())) {
$customFields['net'] = (float) $productEntity->getPrices()->first()->getPrice()->first()->getNet();
$customFields['brut'] = (float) $productEntity->getPrices()->first()->getPrice()->first()->getGross();
$productEntity->setCustomFields($customFields);
$productEntity->setCustomFields($customFields);
if(!$oCurrentSession->get('first_zero_tax_hit')) {
$oCurrentSession->set('first_zero_tax_hit', true);
$bFirstZeroTaxHit = true;
}
}
}
}
if($bFirstZeroTaxHit) {
$oCurrentSession->set('zero_tax_consent', true);
$oCurrentSession->set('zero_tax_waiver', false);
}
} else {
// $oCurrentSession->set('zero_tax_consent', false);
}
if ($sRoute == 'frontend.detail.page') {
foreach ( $event->getEntities() as $productEntity ) {
$iTaxRate = intval($productEntity->getTax()->getTaxRate());
if (empty($iTaxRate)) {
$customFields = $productEntity->getCustomFields();
$customFields['zeroTax'] = true;
$productEntity->setCustomFields($customFields);
}
}
}
if($sRoute == 'frontend.detail.page' || $sRoute == 'frontend.expert.search'){
return;
}
if ( !($event->getContext()->getSource() instanceof \Shopware\Core\Framework\Api\Context\SalesChannelApiSource) ) {
return;
}
}
/**
* @param $key
* @param $value
* @param \DateInterval $expiresAfter
* @throws InvalidArgumentException
*/
public function cacheObject($key, $value, \DateInterval $expiresAfter)
{
$cache = $this->cache;
if( (bool)getenv('APP_STAGE')) {
$cache = $this->cacheStage;
}
$item = $cache->getItem($key);
$item->set($value);
$item->expiresAfter($expiresAfter);
$cache->save($item);
}
/**
* @param $key
* @return mixed|null
* @throws InvalidArgumentException
*/
public function retrieveObject($key, $bReturn = false)
{
$cache = $this->cache;
if( (bool)getenv('APP_STAGE')) {
$cache = $this->cacheStage;
}
$item = $cache->getItem($key);
if ($item->isHit()) {
return $item->get();
}
return $this->getAllProductPrices($bReturn);
}
/**
* @param false $bReturn
* @return array
* @throws \Doctrine\DBAL\Exception
*/
private function getAllProductPrices(bool $bReturn = false): array
{
$sql = <<<SQL
SELECT
HEX(p.id) as id,
product_number,
JSON_EXTRACT(p.price, CONCAT('$.', JSON_UNQUOTE(JSON_EXTRACT(JSON_KEYS(p.price, '$[0]'), "$[0]")), '.gross')) AS gross_pp,
JSON_EXTRACT(p.price, CONCAT('$.', JSON_UNQUOTE(JSON_EXTRACT(JSON_KEYS(p.price, '$[0]'), "$[0]")), '.net')) AS net_pp,
JSON_EXTRACT(pp.price, CONCAT('$.', JSON_UNQUOTE(JSON_EXTRACT(JSON_KEYS(pp.price, '$[0]'), "$[0]")), '.gross')) AS gross,
JSON_EXTRACT(pp.price, CONCAT('$.', JSON_UNQUOTE(JSON_EXTRACT(JSON_KEYS(pp.price, '$[0]'), "$[0]")), '.net')) AS net
FROM product p, product_translation pt, product_price pp
WHERE
p.id =pt.product_id
AND p.version_id = pt.product_version_id
AND p.id =pp.product_id
AND p.version_id = pp.product_version_id
AND JSON_EXTRACT(pt.custom_fields, '$.taxrate') = 0
SQL;
$aResultsObject = [];
//get the number of affected rows
$aResults = $this->connection->executeQuery($sql)->fetchAll();
foreach ($aResults as $aResult) {
$aResultsObject[strtolower($aResult['id'])] = [
"product_number" => $aResult['product_number'],
"gross" => (float) $aResult['gross'],
"net" => (float) $aResult['net']
];
}
if($bReturn) {
return $aResultsObject;
}
$sRedisProductsCacheKey = self::ALL_ZERO_TAX_GROSS_NET_LIVE;
if( (bool)getenv('APP_STAGE')) {
$sRedisProductsCacheKey = self::ALL_ZERO_TAX_GROSS_NET_STG;
}
$this->cacheObject($sRedisProductsCacheKey, $aResultsObject, new \DateInterval('PT10S'));
return $aResultsObject;
}
private function getGoogleBotIps()
{
try {
$item = $this->cache->getItem('sGoogleBotIps');
if (!$item->isHit()) {
$client = new \GuzzleHttp\Client();
$response = $client->get(self::GOOGLE_BOT_IP_RANGES);
/** @var \GuzzleHttp\Psr7\Stream $rBody */
$aBotLoads = json_decode($response->getBody()->getContents(), true)['prefixes'] ;
$aOutputIps = array_map(
function (array $elem) {
if(array_key_exists('ipv4Prefix', $elem)) {
$range = array();
$cidr = explode('/', reset($elem));
$range[0] = long2ip((ip2long($cidr[0])) & ((-1 << (32 - (int)$cidr[1]))));
$range[1] = long2ip((ip2long($range[0])) + pow(2, (32 - (int)$cidr[1])) - 1);
return self::generateIps('ipv4Prefix', $range);
}
},
$aBotLoads
);
$aOutputIps = array_filter($aOutputIps);
$aOutputIps = array_values($aOutputIps);
$aIps = [];
foreach($aOutputIps as $key=>&$aIpBlock){
$aIps = array_merge($aIps, $aIpBlock);
}
$this->cacheObject('sGoogleBotIps', $aIps, new \DateInterval('P1D'));
return $aIps;
}
return $item;
} catch (InvalidArgumentException $e) {
}
return true;
}
/**
* @param string $sType
* @param array $range
* @return array
*/
public static function generateIps(string $sType, array $range): array
{
$aIps = [];
$aStart = explode('.', $range[0]);
$aEnd = explode('.', $range[1]);
$iStart = end($aStart);
$iEnd = end($aEnd);
array_pop($aStart);
$sBase = implode('.', $aStart);
for($i=$iStart; $i<=$iEnd; $i++) {
$aIps[] = $sBase.'.'.$i;
}
return $aIps;
}
}