<?php declare(strict_types=1);
namespace Virgin\VirginCustomElement\Storefront\Subscriber;
use DateInterval;
use DateTimeImmutable;
use Doctrine\DBAL\Connection;
use Exception;
use PDO;
use Shopware\Core\Checkout\Promotion\Gateway\Template\ActiveDateRange;
use Shopware\Core\Checkout\Promotion\PromotionEntity;
use Shopware\Core\Content\Cms\Aggregate\CmsSlot\CmsSlotEntity;
use Shopware\Core\Content\Cms\CmsPageEvents;
use Shopware\Core\Content\Product\ProductEntity;
use Shopware\Core\Defaults;
use Shopware\Core\Framework\Api\Context\AdminApiSource;
use Shopware\Core\Framework\Context;
use Shopware\Core\Framework\DataAbstractionLayer\EntityRepository;
use Shopware\Core\Framework\DataAbstractionLayer\Event\EntityLoadedEvent;
use Shopware\Core\Framework\DataAbstractionLayer\Pricing\Price;
use Shopware\Core\Framework\DataAbstractionLayer\Pricing\PriceCollection;
use Shopware\Core\Framework\DataAbstractionLayer\Search\Criteria;
use Shopware\Core\Framework\DataAbstractionLayer\Search\Filter\ContainsFilter;
use Shopware\Core\Framework\DataAbstractionLayer\Search\Filter\EqualsFilter;
use Shopware\Core\Framework\Uuid\Uuid;
use Shopware\Core\System\SalesChannel\Context\CachedSalesChannelContextFactory;
use Shopware\Core\System\SalesChannel\SalesChannelContext;
use Shopware\Core\System\Tag\TagCollection;
use Shopware\Core\System\Tag\TagEntity;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\HttpFoundation\RequestStack;
use Symfony\Component\HttpFoundation\Session\SessionInterface;
use Virgin\ProductModelExtension\Custom\Club\ClubEntity;
use Virgin\ProductModelExtension\Services\ClubService;
use Virgin\ProductModelExtension\Services\ProductService;
use Virgin\SubscriptionsConfigurator\Utils\Services\SubscriptionsConfiguratorService;
use Virgin\SystemIntegration\Exception\VirginApiException;
use Virgin\SystemIntegration\Services\RestApiClient;
class StorefrontSubscriber implements EventSubscriberInterface
{
const string VIRGIN_CMS_SLOT_ENTITY_TYPE_PRODUCT_BOX = 'virgin-product-box';
const string VIRGIN_CMS_SLOT_ENTITY_TYPE_REGISTRATION_FORM = 'virgin-registration-form';
const string VIRGIN_CMS_SLOT_ENTITY_TYPE_CLUB_SELECT_FORM = 'virgin-club-select-form';
const string SPECIAL_PROMO_PREFIX = 'promo_';
const string SPECIAL_PROMO_TAG = 'special_promo';
const string CONFIG_LANDING_TAG = 'config';
const string VIRGIN_CMS_SLOT_ENTITY_TYPE_LANDING_CONFIG = 'virgin-landing-config';
/**
* @var ClubService
*/
private ClubService $clubService;
/**
* @var SessionInterface
*/
private SessionInterface $session;
/**
* @var EntityRepository
*/
private EntityRepository $promotionRepository;
/**
* @var EntityRepository
*/
private EntityRepository $salesChannelRepository;
/**
* @var CachedSalesChannelContextFactory
*/
private CachedSalesChannelContextFactory $cachedSalesChannelContextFactory;
/**
* @var SubscriptionsConfiguratorService
*/
private SubscriptionsConfiguratorService $subscriptionsConfigurationService;
/**
* @var RestApiClient
*/
private RestApiClient $restApiClient;
/**
* @var EntityRepository
*/
private EntityRepository $tagRepository;
/**
* @var EntityRepository
*/
private EntityRepository $categoryRepository;
/**
* @var EntityRepository
*/
private EntityRepository $cmsSlotTranslationRepository;
/** @var Connection */
private Connection $connection;
/**
* @var RequestStack
*/
private RequestStack $requestStack;
/**
* @var ProductService
*/
private ProductService $productService;
public function __construct(
ClubService $clubService,
EntityRepository $promotionRepository,
EntityRepository $salesChannelRepository,
CachedSalesChannelContextFactory $cachedSalesChannelContextFactory,
SubscriptionsConfiguratorService $subscriptionsConfigurationService,
SessionInterface $session,
EntityRepository $tagRepository,
RestApiClient $restApiClient,
EntityRepository $categoryRepository,
EntityRepository $cmsSlotTranslationRepository,
Connection $connection,
RequestStack $requestStack,
ProductService $productService
) {
$this->clubService = $clubService;
$this->promotionRepository = $promotionRepository;
$this->salesChannelRepository = $salesChannelRepository;
$this->cachedSalesChannelContextFactory = $cachedSalesChannelContextFactory;
$this->subscriptionsConfigurationService = $subscriptionsConfigurationService;
$this->session = $session;
$this->tagRepository = $tagRepository;
$this->restApiClient = $restApiClient;
$this->categoryRepository = $categoryRepository;
$this->cmsSlotTranslationRepository = $cmsSlotTranslationRepository;
$this->connection = $connection;
$this->requestStack = $requestStack;
$this->productService = $productService;
}
public static function getSubscribedEvents(): array
{
return [
CmsPageEvents::SLOT_LOADED_EVENT => 'addProductInfo',
];
}
/**
* @param EntityLoadedEvent $event
* @throws VirginApiException
* @throws Exception
*/
public function addProductInfo(EntityLoadedEvent $event): void
{
$context = $event->getContext();
if ($context->getSource() instanceof AdminApiSource && $context->getSource()->isAdmin() || $this->requestStack->getCurrentRequest()->attributes->get('_route') !== 'frontend.navigation.page')
return;
$salesChannelContext = $this->getSalesChannelContext($context);
//get landing config if there's any
$landingConfig = $this->getLandingConfig($event);
$virginCustomEntities = $this->getVirginCustomEntities($event);
if (!$virginCustomEntities){
return;
}
//expiration date check
//todo spostare nel context
if ($landingConfig && $landingConfig['expirationDate']) {
$isExpired = new \DateTime($landingConfig['expirationDate']) < new \DateTime();
if ($isExpired) {
foreach ($virginCustomEntities as $entity){
$entity->setCustomFields([
'isExpired' => true,
'redirectUrl' => $landingConfig['redirectUrl'] ?? 'https://www.virginactive.it/'
]);
}
}
}
$selectedClubGuids = !empty($landingConfig) && array_key_exists('clubArray', $landingConfig) && $landingConfig['clubArray'] ? $landingConfig['clubArray'] : [];
/** @var ClubEntity[] $clubs */
$clubs = $this->clubService->getClubList($salesChannelContext, $selectedClubGuids, empty($selectedClubGuids));
switch ($virginCustomEntities[0]->getType()){
case self::VIRGIN_CMS_SLOT_ENTITY_TYPE_PRODUCT_BOX:
//get promotion
$promotionCriteria = new Criteria();
$promotionCriteria->addAssociation('virginPromotionAttributes');
$promotionCriteria->addFilter(new EqualsFilter('virginPromotionAttributes.isSpecialPromoFlow', true));
$promotionCriteria->addFilter(new EqualsFilter('virginPromotionAttributes.isExerpPromotion', true));
$promotionCriteria->addFilter(new ActiveDateRange());
$promotionCriteria->addFilter(new EqualsFilter('active', true));
/** @var PromotionEntity $specialFlowPromotion */
$specialFlowPromotion = $this->promotionRepository->search(
$promotionCriteria,
$context
)->first();
$isSpecialPromoFlow = false;
$isDigitalProds = true;
$clubId = null;
$realProductEntities = [];
$tagIds = [];
$landingPagePromoTagName = $this::SPECIAL_PROMO_PREFIX;
if ($specialFlowPromotion instanceof PromotionEntity) {
$isSpecialPromoFlow = true;
$isLandingConsultant = $landingConfig && array_key_exists('landingType', $landingConfig) && $landingConfig['landingType'] == 'consultant';
$isLandingBocconi = $landingConfig && array_key_exists('landingType', $landingConfig) && $landingConfig['landingType'] == 'bocconi';
$isLandingSaltafila = $landingConfig && array_key_exists('landingType', $landingConfig) && $landingConfig['landingType'] == 'saltafila';
if ($isLandingConsultant){
$consultantId = $this->getConsultantId();
if (!$consultantId){
$this->redirectToHomepage();
}
list($centerId, $userId)= explode("p", $consultantId);
$selectedClub = $this->clubService->getClubByExerpId($centerId, $salesChannelContext);
$clubId = $selectedClub->getId();
if ($clubId == null){
$this->redirectToHomepage();
}
$this->session->set("consultantId", $consultantId);
} else if ($isLandingSaltafila){
$appointmentId = $this->getAppointmentId();
$clubId = $this->getClubId();
if (!$clubId){
$this->redirectToHomepage();
}
$selectedClub = $this->clubService->getClubByExerpId($clubId, $salesChannelContext);
$clubId = $selectedClub->getId();
if ($clubId == null){
$this->redirectToHomepage();
}
if ($appointmentId){
$this->session->set("appointmentId", $appointmentId);
}
}
else if (!empty($this->session->get('clubId'))) {
//chatbot only
$clubId = $this->session->get('clubId');
$selectedClub = $this->clubService->getClubById($clubId, $salesChannelContext);
} else {
$selectedClub = $clubs ? reset($clubs) : $this->clubService->getDefaultClub($salesChannelContext);
$clubId = $selectedClub->getId();
}
$clubName = $selectedClub->getName();
$this->session->set('clubName', $clubName);
$context->assign(["selectedClub" => $selectedClub]);
$context->assign(["clubName" => $clubName]);
$context->assign(["clubList" => $this->subscriptionsConfigurationService->getClubFacet($salesChannelContext)]);
$context->assign(["showClubName" => $landingConfig && isset($landingConfig['showClubName']) ? $landingConfig['showClubName'] : false]);
$landingPagePromoTagName = $this->getCurrentLandingPageSpecialPromoTagName();
$tagIds = $this->getLandingPagePromoTagIds($context, $landingPagePromoTagName);
if ($isLandingBocconi){
$bocconiToken = $this->getBocconiToken();
if (!$bocconiToken){
$this->redirectToHomepage();
}
$bocconiUser = $this->getBocconiUserInfo($bocconiToken);
if (!$bocconiUser){
$this->redirectToHomepage();
}
$bocconiTagName = $bocconiUser['campaignCode'];
if ($bocconiTagName){
$criteria = new Criteria();
$criteria->addFilter(new ContainsFilter('name', $bocconiTagName));
$idTag = $this->tagRepository->searchIds($criteria, $context)->firstId();
if ($idTag){
array_unshift($tagIds, $idTag);
}
}
}
}
if ($landingConfig && isset($landingConfig['landingSelectClubUrl'])){
$context->assign(["landingSelectClubUrl" => $landingConfig['landingSelectClubUrl']]);
}
foreach ($virginCustomEntities as $entity){
//get products assigned via editor
// 1:1 relation entity:product
// can be fake or real
$productEntity = $this->productService->getProductById($entity->getConfig()['product']['value'], $salesChannelContext);
if ($productEntity) {
$productTagsArray = $productEntity->getTags()->getElements();
if (count($productTagsArray) > 0) {
$specialPromoFakeProductTag = reset($productTagsArray);
$isDigitalProds = strpos($specialPromoFakeProductTag->getName(), self::SPECIAL_PROMO_TAG) === false;
}
if ($isDigitalProds) {
$realProductEntities[] = $productEntity;
}
}
}
if ($isDigitalProds) {
$this->session->set("isDigital", true);
} else {
$this->session->set("isDigital", false);
}
// i prodotti digitali non hanno tagsIds quindi è in or
if (empty($realProductEntities) && $isSpecialPromoFlow) {
$realProductEntities = $this->productService->getProductsByTags($clubId, $tagIds, $salesChannelContext);
}
$this->getProductBoxProducts(
$realProductEntities,
$landingPagePromoTagName,
$salesChannelContext,
$isSpecialPromoFlow,
$specialFlowPromotion,
$virginCustomEntities,
$isDigitalProds
);
break;
case self::VIRGIN_CMS_SLOT_ENTITY_TYPE_CLUB_SELECT_FORM:
case self::VIRGIN_CMS_SLOT_ENTITY_TYPE_REGISTRATION_FORM:
$entity = $virginCustomEntities[0];
$entity->setCustomFields([
'clubs' => $clubs,
'landingCardsUrl' => $landingConfig['landingCardsUrl'] ?? '',
'landingSelectClubUrl' => $landingConfig['landingSelectClubUrl'] ?? '',
]);
break;
default:
}
}
/**
* @param Context $context
* @return SalesChannelContext
* @throws \JsonException
*/
private function getSalesChannelContext(Context $context): SalesChannelContext
{
$criteria = new Criteria();
$criteria
->addFilter(new EqualsFilter('typeId', Defaults::SALES_CHANNEL_TYPE_STOREFRONT))
->addFilter(new EqualsFilter('active', true));
$salesChannelId = $this->salesChannelRepository->searchIds($criteria, $context)->firstId();
return $this->cachedSalesChannelContextFactory->create(Uuid::randomHex(), $salesChannelId);
}
/**
* @return string
*/
private function getCurrentLandingPageSpecialPromoTagName(): string
{
if ($currentNavigationPageTags = $this->session->get('currentNavigationPageTags')) {
/** @var TagEntity $landingPageTag */
foreach ($currentNavigationPageTags->getElements() as $landingPageTag) {
if (str_contains($landingPageTagName = $landingPageTag->getName(), self::SPECIAL_PROMO_PREFIX)) {
return $landingPageTagName;
}
}
}
return "";
}
/**
* ritorna true se la landing page ha il tag che la identifica per avere il blocco di configurazione
* @return bool
*/
public function isConfigLanding(): bool
{
if ($currentNavigationPageTags = $this->session->get('currentNavigationPageTags')) {
/** @var TagEntity $tag */
foreach ($currentNavigationPageTags->getElements() as $tag) {
if (strcmp($tag->getName(), self::CONFIG_LANDING_TAG)===0) {
return true;
}
}
}
return false;
}
/**
* @param Context $context
* @param string $landingPagePromoTagName
* @return array
*/
private function getLandingPagePromoTagIds(Context $context, string $landingPagePromoTagName): array
{
if (!$landingPagePromoTagName) return [];
$criteria = new Criteria();
$criteria->addFilter(new ContainsFilter('name', $landingPagePromoTagName . "_"));
return $this->tagRepository->searchIds($criteria, $context)->getIds();
}
/**
* @param string $landingPagePromoTagName
* @param TagCollection $tags
* @return int
*/
private function getLandingPagePromoTagIndex(string $landingPagePromoTagName, TagCollection $tags): int
{
if ($landingPagePromoTagName) {
foreach ($tags as $tag) {
if (str_contains($tag->getName(), $landingPagePromoTagName) && count(explode('_', $tag->getName())) == 3)
return (int)explode('_', $tag->getName())[2];
}
}
return 0;
}
/**
* @param array $realProductEntities
* @param string $landingPagePromoTagName
* @param SalesChannelContext $salesChannelContext
* @param bool $isSpecialPromoFlow
* @param PromotionEntity|null $specialFlowPromotion
* @param $entities
* @param bool $isDigitalProds
* @throws Exception
*/
public function getProductBoxProducts(array $realProductEntities, string $landingPagePromoTagName, SalesChannelContext $salesChannelContext, bool $isSpecialPromoFlow, ?PromotionEntity $specialFlowPromotion, $entities, $isDigitalProds): void
{
$products = [];
/** @var ProductEntity $realProductEntity */
foreach ($realProductEntities as $realProductEntity) {
$index = $this->getLandingPagePromoTagIndex($landingPagePromoTagName, $realProductEntity->getTags());
try {
$type = $realProductEntity->getCustomFields()['virgin_exerp_type'];
$prorataDate = (new DateTimeImmutable())->add(new DateInterval('PT0H'))->format('Y-m-d');
if (array_key_exists("free_trial_days", $realProductEntity->getCustomFields())) {
$prorataDate = (new DateTimeImmutable())->add(new DateInterval('PT' . ($realProductEntity->getCustomFields()['free_trial_days'] * 24) . 'H'))->format('Y-m-d');
}
$subReq = [
'campainCode' => '',
'centerId' => $realProductEntity->getCustomFields()['virgin_exerp_centerid'],
'clearingHouseType' => $this->restApiClient::CLEARING_HOUSE_EFT,
'personType' => $this->restApiClient::PERSONTYPE_PRIVATE,
'starDate' => $prorataDate,
'subscriptionId' => $realProductEntity->getCustomFields()['virgin_exerp_productid'],
];
$response = $this->restApiClient->getSubscritionContractDetails($subReq);
if ($type == "EFT") {
$exerpPrice = $response->normalPeriodPrice ?? $realProductEntity->getPrice()->first()->getGross();
}
if ($type == "CASH") {
$exerpPrice = $response->totalAmount ?? $realProductEntity->getPrice()->first()->getGross();
}
if ($type == "GIFTCARD") {
$exerpPrice = $realProductEntity->getPrice()->first()->getGross();
}
$priceCollection = new PriceCollection();
$priceEntity = new Price(
Defaults::CURRENCY,
(float) $exerpPrice,
(float) $exerpPrice,
$realProductEntity->getPrice()->getCurrencyPrice(Defaults::CURRENCY)->getLinked()
);
$priceCollection->add($priceEntity);
$realProductEntity->setPrice($priceCollection);
} catch (VirginApiException $e) {
$isExerpDown = true;
}
$productCardInformation = $this->productService->getProductCardInformation($realProductEntity, $salesChannelContext);
$products[] = [
'product' => $realProductEntity,
'position' => $index,
'config' => [
'promotion' => $isSpecialPromoFlow ? current($specialFlowPromotion->getExtension('virginPromotionAttributes')->getElements()) : null,
'prices' => $productCardInformation['prices'],
'productCardInformation' => $productCardInformation,
'workouts' => $productCardInformation['workouts'],
'services' => $productCardInformation['services'],
'directFlow' => isset($_SESSION['isDirectFlow']),
'exerpDown' => $isExerpDown ?? null
],
'productCardInformation' => $productCardInformation,
];
}
usort($products, static function (array $a, array $b) {
return $a['position'] <=> $b['position'];
});
if ($isDigitalProds) {
// se $isDigitalProds ogni entity avrà un solo array contente un solo product
$products = array_values($products);
foreach ($entities as $index => $entity) {
$entity->getCustomFields() ? $entity->setCustomFields(
array_merge(
$entity->getCustomFields(),
['products' => [$products[$index]]])
) : $entity->setCustomFields(['products' => [$products[$index]]]);
}
} else {
// altrimenti ogni entity avrà lo stesso array di prodotti
foreach ($entities as $entity) {
$entity->getCustomFields() ? $entity->setCustomFields(
array_merge(
$entity->getCustomFields(),
['products' => $products])
) : $entity->setCustomFields(['products' => $products]);
}
}
}
/**
* @param $entities
* @return array
*/
public function getProductBoxEntities($entities): array
{
$prodBoxEntities = [];
foreach ($entities as $entity) {
if ($entity->getType() === self::VIRGIN_CMS_SLOT_ENTITY_TYPE_PRODUCT_BOX) {
$prodBoxEntities[] = $entity;
}
}
return $prodBoxEntities;
}
private function getConsultantId(){
if (isset($_GET['id'])){
$pattern = '/^\d{3}p[0-9]+$/i';
if (preg_match($pattern, $_GET['id'])){
return $_GET['id'];
}
}
return null;
}
/**
* @return mixed|null
*/
private function getBocconiToken(): mixed
{
if ($this->session->has("bocconiToken")){
$token = $this->session->get("bocconiToken");
$pattern = '/[A-Za-z0-9]+/i';
if (preg_match($pattern, $token)){
return $token;
}
}
return null;
}
/**
* @param string $bocconiToken
* @throws VirginApiException
*/
public function getBocconiUserInfo(string $bocconiToken)
{
return $this->restApiClient->getBocconiLead($bocconiToken);
}
/**
* @return mixed|null
*/
private function getAppointmentId(){
if (isset($_GET['appointmentId'])){
return $_GET['appointmentId'];
}
return null;
}
/**
* @return mixed|null
*/
private function getClubId(){
if (isset($_GET['clubId'])){
$pattern = '/^\d{3}+$/i';
if (preg_match($pattern, $_GET['clubId'])){
return $_GET['clubId'];
}
}
return null;
}
/**
* @param EntityLoadedEvent $event
* @return array|null
*/
private function getLandingConfig(EntityLoadedEvent $event): ? array
{
$context = $event->getContext();
$mapCallbackFunction = function ($el){
return $el['value'];
};
//vedere su quale landing siamo tramite tag "config/cards"
if ($this->isConfigLanding()){
//retrieve della config
foreach ($event->getEntities() as $entity){
/** @var CmsSlotEntity $entity */
if ($entity->getType()==="virgin-landing-config"){
return array_map($mapCallbackFunction, $entity->getConfig());
}
}
} else {
//retrieve del tag della promo
$landingPagePromoTagName = $this->getCurrentLandingPageSpecialPromoTagName();
//tramite tag promo prendere trio di landing
$cmsPageId = $this->getConfigCmsPageIdFromTag($landingPagePromoTagName, $context);
//recuperare la config
if ($cmsPageId){
$slotId = $this->getConfigSlotId($cmsPageId);
if ($slotId){
return array_map($mapCallbackFunction, $this->cmsSlotTranslationRepository->search(
(new Criteria())->addFilter(new EqualsFilter('cmsSlotId', $slotId), new EqualsFilter('languageId', $context->getLanguageId())),
$context
)->first()->getConfig());
}
}
}
return null;
}
/**
* @return void
*/
private function redirectToHomepage(): void
{
echo '
<script type="text/javascript">
window.location = "https://www.virginactive.it/"
</script>';
}
/**
* @param string $tag
* @param Context $context
* @return string|null
*/
public function getConfigCmsPageIdFromTag(string $tag, Context $context): ? string
{
$criteria = new Criteria();
$criteria->addFilter(new EqualsFilter('tags.name', $tag));
$criteria->addFilter(new EqualsFilter('tags.name', self::CONFIG_LANDING_TAG));
$category = $this->categoryRepository->search($criteria, $context)->first();
return $category ? $category->getCmsPageId() : null;
}
/**
* @param string $cmsPageId
* @return string|boolean
* @throws \Doctrine\DBAL\Exception
* @throws \Doctrine\DBAL\Driver\Exception
*/
private function getConfigSlotId(string $cmsPageId): bool|string
{
$builder = $this->connection->createQueryBuilder();
return $builder->select(['LOWER(HEX(slot.id))'])
->from('cms_slot', 'slot')
->leftJoin('slot','cms_block','block','slot.cms_block_id = block.id')
->leftJoin('block','cms_section','section','block.cms_section_id = section.id')
->where('LOWER(HEX(section.cms_page_id)) = :cmsPageId')
->andWhere('slot.type = :type')
->setParameter('cmsPageId', $cmsPageId)
->setParameter('type', self::VIRGIN_CMS_SLOT_ENTITY_TYPE_LANDING_CONFIG)
->execute()
->fetchOne();
}
/**
* @param EntityLoadedEvent $event
* @return CmsSlotEntity[]
*/
private function getVirginCustomEntities(EntityLoadedEvent $event): array
{
$entities = [];
/** @var CmsSlotEntity $entity */
foreach ($event->getEntities() as $entity) {
if ($entity->getType() === self::VIRGIN_CMS_SLOT_ENTITY_TYPE_REGISTRATION_FORM ||
$entity->getType() === self::VIRGIN_CMS_SLOT_ENTITY_TYPE_CLUB_SELECT_FORM ||
$entity->getType() === self::VIRGIN_CMS_SLOT_ENTITY_TYPE_PRODUCT_BOX
) {
$entities[] = $entity;
}
}
return $entities;
}
}