<?php
namespace App\EventSubscriber\Doctrine;
use App\Enum\ActivityLogCategoryEnum;
use App\Enum\ActivityLogMethodEnum;
use App\Enum\OrganizationStatusPayedEnum;
use App\Enum\ProductKeyEnum;
use App\Enum\SubscriptionPackEnum;
use App\Events\SubscriptionChangedEvent;
use App\Repository\OrganizationRepository;
use App\Repository\SubscriptionModelRepository;
use App\Repository\SubscriptionRepository;
use App\Service\OrganizationUtils;
use App\Service\SegmentAPI;
use App\Service\SubscriptionUtils;
use App\Traits\SentryNotifyTrait;
use Doctrine\Common\EventSubscriber;
use Doctrine\ORM\EntityManagerInterface;
use Doctrine\ORM\Event\LifecycleEventArgs;
use Doctrine\ORM\Event\OnFlushEventArgs;
use Doctrine\ORM\Event\PreUpdateEventArgs;
use Doctrine\ORM\UnitOfWork;
use Evo\Infrastructure\MappingORM\ActivityLog;
use Evo\Infrastructure\MappingORM\Organization;
use Evo\Infrastructure\MappingORM\Product;
use Evo\Infrastructure\MappingORM\Service;
use Evo\Infrastructure\MappingORM\Subscription;
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\RequestStack;
use Symfony\Component\Security\Core\Authentication\Token\SwitchUserToken;
use Symfony\Component\Security\Core\Security;
use Symfony\Component\Security\Core\User\UserInterface;
class SubscriptionSubscriber implements EventSubscriber
{
use SentryNotifyTrait;
public const SERVICE_NAME = '[ SUBSCRIPTION SUBSCRIBER ] ::';
private RequestStack $requestStack;
private SegmentAPI $segment;
private OrganizationUtils $organizationUtils;
private EntityManagerInterface $em;
private SubscriptionUtils $subscriptionUtils;
private Security $security;
private EventDispatcherInterface $eventDispatcher;
private SubscriptionModelRepository $subscriptionModelRepository;
private EntityManagerInterface $entityManager;
public function __construct(
RequestStack $requestStack,
SegmentAPI $segmentAPI,
OrganizationUtils $organizationUtils,
SubscriptionUtils $subscriptionUtils,
Security $security,
EntityManagerInterface $em,
EventDispatcherInterface $eventDispatcher,
SubscriptionModelRepository $subscriptionModelRepository,
EntityManagerInterface $entityManager
) {
$this->requestStack = $requestStack;
$this->segment = $segmentAPI;
$this->organizationUtils = $organizationUtils;
$this->subscriptionUtils = $subscriptionUtils;
$this->security = $security;
$this->em = $em;
$this->eventDispatcher = $eventDispatcher;
$this->subscriptionModelRepository = $subscriptionModelRepository;
$this->entityManager = $entityManager;
}
public function getSubscribedEvents(): array
{
return [
'preUpdate',
'prePersist',
'postPersist',
'postUpdate',
'onFlush',
];
}
private array $processedServices = [];
private bool $isFlushing = false;
public function onFlush(OnFlushEventArgs $args): void
{
if ($this->isFlushing) {
return;
}
$this->isFlushing = true;
$em = $args->getEntityManager();
$uow = $em->getUnitOfWork();
// Détecter les ajouts, mises à jour et suppressions
foreach ($uow->getScheduledEntityInsertions() as $entity) {
if ($entity instanceof Service) {
$this->handleServiceInsertion($entity);
}
}
foreach ($uow->getScheduledEntityDeletions() as $entity) {
if ($entity instanceof Service) {
$this->handleServiceDeletion($entity);
}
}
// Récupérer les entités Subscription modifiées
foreach ($uow->getScheduledEntityUpdates() as $entity) {
if ($entity instanceof Subscription) {
if (!$entity->getOrganization()) {
continue;
}
$this->checkServicesChanges($entity, $em, $uow);
$this->checkSubscriptionChanges($entity, $uow);
}
}
// Réinitialiser les services traités après flush
$this->processedServices = [];
$this->isFlushing = false;
}
private function checkSubscriptionChanges(Subscription $subscription, UnitOfWork $uow): void
{
// Récupérer les changements sur Subscription
$changes = $uow->getEntityChangeSet($subscription);
$changeSet = [];
foreach ($changes as $field => $change) {
// On surveille les champs spécifiques : prix, type (fréquence), discount
if (in_array($field, ['priceWithoutTax', 'type', 'discountWithoutTax'], true)) {
$oldValue = $change[0] instanceof \DateTime ? $change[0]->format('Y-m-d H:i:s') : $change[0];
$newValue = $change[1] instanceof \DateTime ? $change[1]->format('Y-m-d H:i:s') : $change[1];
// Si la valeur a changé, on l'ajoute au tableau des modifications
if ($oldValue !== $newValue) {
$changeSet[$field] = [
$oldValue => $newValue,
];
}
}
}
// Si des changements ont été détectés, on crée un log d'activité
if (!empty($changeSet)) {
try {
$activityLog = ActivityLog::create(
$this->getUser(),
ActivityLogMethodEnum::UPDATE,
'Changements sur L\'abonnement',
ActivityLogCategoryEnum::SUBSCRIPTION,
$changeSet,
$subscription->getOrganization(),
null,
null
);
$this->em->persist($activityLog);
$this->em->flush();
} catch (\Exception $e) {
$this->captureSentryException($e);
}
}
}
private function getUser(): ?UserInterface
{
$request = $this->requestStack->getCurrentRequest();
$token = $this->security->getToken();
$user = null;
if ($request instanceof Request) {
if ($token instanceof SwitchUserToken) {
$user = $token->getOriginalToken()->getUser();
} else {
$user = $this->security->getUser();
}
}
return $user;
}
private function handleServiceInsertion(Service $service): void
{
if (null === $service->getSubscription() || null === $service->getSubscription()->getOrganization()) {
return;
}
try {
$activityLog = ActivityLog::create(
$this->getUser(),
'CREATION',
'Service ajouté: '.$service->getProduct()->getName(),
ActivityLogCategoryEnum::SUBSCRIPTION,
[
'Type' => ['' => $service->getType()],
'Prix hors taxes' => ['' => $service->getPriceWithoutTax()],
'Période d\'essai' => ['' => $service->isTrial()],
'Retirer après l\'essai' => ['' => $service->isRemoveAfterTrial()],
'Durée de la période d\'essai' => ['' => $service->getTrialPeriodLength()],
],
$service->getSubscription()->getOrganization(),
null,
null
);
$this->em->persist($activityLog);
$this->em->flush();
} catch (\Exception $e) {
$this->captureSentryException($e);
}
}
private function handleServiceDeletion(Service $service): void
{
if (null === $service->getSubscription() || null === $service->getSubscription()->getOrganization()) {
return;
}
try {
$activityLog = ActivityLog::create(
$this->getUser(),
ActivityLogMethodEnum::UPDATE,
'Service supprimé: '.$service->getProduct()->getName(),
ActivityLogCategoryEnum::SUBSCRIPTION,
[
'Type' => [$service->getType() => '-'],
'Prix hors taxes' => [$service->getPriceWithoutTax() => '-'],
'Période d\'essai' => [$service->isTrial() => '-'],
'Retirer après l\'essai' => [$service->isRemoveAfterTrial() => '-'],
'Durée de la période d\'essai' => [$service->getTrialPeriodLength() => '-'],
],
$service->getSubscription()->getOrganization(),
null,
null
);
$this->em->persist($activityLog);
$this->em->flush();
} catch (\Exception $e) {
$this->captureSentryException($e);
}
}
private function checkServicesChanges(Subscription $subscription, $em, UnitOfWork $uow): void
{
// Récupérer les services associés à l'abonnement
$services = $subscription->getServices();
foreach ($services as $service) {
$serviceId = $service->getId();
$serviceName = $service->getProduct()->getName();
// Vérifier si le service a déjà été traité
if (in_array($serviceId, $this->processedServices, true)) {
// Si ce service a déjà été traité, passer au suivant
continue;
}
// Ajouter ce service à la liste des services déjà traités
$this->processedServices[] = $serviceId;
// Vérifier si le service a été modifié
if ($uow->isScheduledForUpdate($service)) {
// Récupérer les changements sur le service
$changes = $uow->getEntityChangeSet($service);
$changeSet = [];
foreach ($changes as $field => $change) {
if (Service::INCLUDED === $service->getType() && 'priceWithoutTax' === $field) {
continue;
}
$oldValue = $change[0] instanceof \DateTime ? $change[0]->format('Y-m-d H:i:s') : $change[0];
$newValue = $change[1] instanceof \DateTime ? $change[1]->format('Y-m-d H:i:s') : $change[1];
if ($oldValue !== $newValue) {
$changeSet[$field] = [
$oldValue => $newValue,
];
}
}
if (!empty($changeSet)) {
$activityLog = ActivityLog::create(
$this->getUser(),
ActivityLogMethodEnum::UPDATE,
"Changements sur service $serviceName",
ActivityLogCategoryEnum::SUBSCRIPTION,
Service::mapFieldNamesToFrench($changeSet),
$service->getSubscription()->getOrganization(),
null,
null
);
$this->em->persist($activityLog);
$this->em->flush();
}
}
}
}
public function postUpdate(LifecycleEventArgs $args): void
{
$entity = $args->getEntity();
if (!$entity instanceof Subscription) {
return;
}
$this->em = $args->getObjectManager();
$uow = $args->getEntityManager()->getUnitOfWork();
$uow->computeChangeSets();
$changeset = $uow->getEntityChangeSet($entity);
if (1 === count($changeset) && isset($changeset['invoiceGenerationDate'])) {
return;
}
$hasBlockedService = $this->subscriptionUtils->checkIfServiceShouldBeBlocked($entity, OrganizationStatusPayedEnum::UNPAID === $entity->getOrganization()->getStatusInvoicePayed());
$this->subscriptionUtils->updateSubscriptionBlockedService($entity, $hasBlockedService);
if (isset($changeset['hasBlockedService'])) {
$this->segment->trackSubscriptionServiceStatus($entity);
}
if (isset($changeset['documentRecoveryDelay'])) {
$this->segment->trackSubscriptionDocumentRecoveryDelay($entity);
}
$generateContrat = isset($changeset['priceWithoutTax'])
|| isset($changeset['discountWithoutTax'])
|| isset($changeset['services'])
|| isset($changeset['subscriptionDeposit']);
if (null !== $entity->getOrganization() && null !== $entity->getPreviousPack()) {
$organization = $entity->getOrganization();
/** @var OrganizationRepository $organizationRepository */
$organizationRepository = $this->em->getRepository(Organization::class);
$organizationRepository->uptadeOrganizationPreviousPack($entity->getPreviousPack(), $organization->getId());
}
if ($generateContrat && !is_null($entity->getOrganization())) {
try {
$this->organizationUtils->generateDocs($entity->getOrganization(), true);
} catch (\Exception $e) {
$this->captureSentryException($e);
}
}
if (null !== $entity->getOrganization() && isset($changeset['packHistory'])) {
/** @var SubscriptionRepository $subscriptionRepository */
$subscriptionRepository = $this->em->getRepository(Subscription::class);
$subscriptionStatus = SubscriptionUtils::isActiveSubscription($entity);
$subscriptionRepository->updateSubscriptionStatus($entity, $subscriptionStatus);
}
// create ctivity log for track the price
$this->trackSubscriptionPack($entity, $changeset);
// Vérifier les changements sur les services
if (isset($changeset['services'])) {
$oldServices = $changeset['services'][0];
$newServices = $changeset['services'][1];
$activityLogChangeSet = [
'SERVICES' => [
'old' => $oldServices,
'new' => $newServices,
],
];
$request = $this->requestStack->getCurrentRequest();
$token = $this->security->getToken();
$user = null;
if ($request instanceof Request) {
if ($token instanceof SwitchUserToken) {
$user = $token->getOriginalToken()->getUser();
} else {
$user = $this->security->getUser();
}
}
$activityLog = ActivityLog::create(
$user,
ActivityLogMethodEnum::UPDATE,
'MODIFICATION SERVICES SUBSCRIPTION ID: '.$entity->getId(),
ActivityLogCategoryEnum::SUBSCRIPTION,
$activityLogChangeSet,
$entity->getOrganization(),
null,
null
);
$this->em->persist($activityLog);
$this->em->flush();
}
}
public function preUpdate(PreUpdateEventArgs $args): void
{
$entity = $args->getEntity();
if (!$entity instanceof Subscription) {
return;
}
$changeSet = $args->getEntityChangeSet();
if (isset($changeSet['organization']) &&
null === $changeSet['organization'][1] &&
isset($_SESSION['remove_subscription'])
) {
$organization = $changeSet['organization'][0];
if ($organization) {
$params = SubscriptionUtils::getSubscriptionDataToSegment($organization);
$documentRIB = $this->organizationUtils->getOrganizationRIB($organization);
if ($documentRIB) {
$this->segment->trackApprovedDocument($documentRIB, $params);
}
unset($_SESSION['remove_subscription']);
}
}
if (1 === count($changeSet) && isset($changeSet['invoiceGenerationDate'])) {
return;
}
if (isset($changeSet['isCancelUsingOffer'])) {
$this->segment->identifyUpdateSubscription($entity, $changeSet);
}
if (isset($changeSet['nextPayment'])) {
$this->segment->identifyNextPayment($entity);
}
if (null === $entity->getServices()) {
$args->getObjectManager()->remove($entity);
}
if ($this->requestStack->getCurrentRequest() && empty($this->requestStack->getCurrentRequest()->get('subscriptionPreUpdated'))) {
$this->requestStack->getCurrentRequest()->query->set('subscriptionPreUpdated', true);
}
$this->subscriptionUtils->addStoreAdditionStoreForDom($entity);
SubscriptionUtils::setPrice($entity);
if (null !== $entity->getOrganization()) {
/** @var Organization $organization */
$organization = $entity->getOrganization();
$params = SubscriptionUtils::getSubscriptionDataToSegment($organization);
$documentRIB = $this->organizationUtils->getOrganizationRIB($organization);
if ($documentRIB && !isset($changeSet['isCancelUsingOffer'])) {
$this->segment->trackApprovedDocument($documentRIB, $params);
}
}
}
private function trackSubscriptionPack(Subscription $subscription, array $changetSet): void
{
$isCourrier = $this->isCourrier();
/**
* Historique pack.
*/
$pack = $this->subscriptionUtils->getPackUniqueKey($subscription);
if ($pack) {
$histories = $subscription->getPackHistory();
$histories[] = $pack;
$subscription->setPackHistory($histories);
}
$previousPack = $subscription->getPreviousPack();
// reperer si le pack a changé
$packHasChanged = $previousPack !== $pack;
// definir si c'est un upsell ou un downsell
if ($packHasChanged) {
$request = $this->requestStack->getCurrentRequest();
$isFromAdmin = false;
if ($request instanceof Request) {
$requestContent = json_decode($request->getContent(), true);
if (isset($requestContent['isAdmin']) && $requestContent['isAdmin']) {
$isFromAdmin = true;
}
}
if (!$isFromAdmin) {
// check if store addition have the good price
$this->checkIfStoreAdditionHaveTheGoodPrice($subscription);
}
$currentPack = $this->subscriptionModelRepository->findOneBy(['uniqueKey' => $pack]);
$previousPack = $this->subscriptionModelRepository->findOneBy(['uniqueKey' => $previousPack]);
if ($currentPack && $previousPack) {
$currentPackCountServices = $currentPack->getIncludedServices()->count();
$previousPackCountServices = $previousPack->getIncludedServices()->count();
$isUpSell = $currentPackCountServices > $previousPackCountServices;
if ($currentPackCountServices === $previousPackCountServices) {
if (
SubscriptionPackEnum::DIGIPACK === $previousPack->getUniqueKey()
&& SubscriptionPackEnum::DOMISCAN === $currentPack->getUniqueKey()
) {
$isUpSell = true;
} elseif (
SubscriptionPackEnum::DOMISCAN === $previousPack->getUniqueKey()
&& SubscriptionPackEnum::DIGIPACK === $currentPack->getUniqueKey()
) {
$isUpSell = false;
}
}
$organization = $subscription->getOrganization();
/** @var ?UserInterface $user */
$user = $this->security->getUser();
$agent = null;
$token = $this->security->getToken();
if ($token instanceof SwitchUserToken) {
$user = $agent = $token->getOriginalToken()->getUser();
} elseif ($user instanceof UserInterface && in_array('ROLE_ADMIN', $user->getRoles(), true)) {
$agent = $user;
}
if ($isUpSell) {
$this->segment->trackUpsell($subscription, $isCourrier);
$event = new SubscriptionChangedEvent($subscription, 'UPSELL', $user, $organization, $agent, $changetSet);
} else {
$this->segment->trackDownsell($subscription, $isCourrier);
$event = new SubscriptionChangedEvent($subscription, 'DOWNSELL', $user, $organization, $agent, $changetSet);
}
/* remove reasonChange when user is admin before create log UPSELL/DOWNSELL */
if ($isFromAdmin) {
$organization->setReasonChange(null);
}
$this->segment->identifyCompany($subscription->getOrganization());
$this->eventDispatcher->dispatch($event, SubscriptionChangedEvent::NAME);
}
} else {
$subscription->setStoredTotalPriceWithoutTax($subscription->getTotalPriceWithoutTax());
$this->entityManager->flush();
}
}
public function prePersist(LifecycleEventArgs $args): void
{
/** @var Subscription $entity */
$entity = $args->getObject();
if (!$entity instanceof Subscription) {
return;
}
$this->em = $args->getObjectManager();
SubscriptionUtils::setPrice($entity);
if (null !== $entity->getOrganization()) {
/** @var Organization $organization */
$organization = $entity->getOrganization();
/**
* Historique pack.
*/
$pack = $this->subscriptionUtils->getPackUniqueKey($entity);
if ($pack) {
$entity->setPackHistory([$pack]);
}
if (isset($_SESSION['hasChanged']) && 1 == $_SESSION['hasChanged']) {
$this->segment->identifyCompany($organization);
unset($_SESSION['hasChanged'], $_SESSION['typeChanged']);
}
}
$this->subscriptionUtils->addStoreAdditionStoreForDom($entity);
}
public function postPersist(LifecycleEventArgs $args): void
{
/** @var Subscription $entity */
$entity = $args->getEntity();
if (!$entity instanceof Subscription) {
return;
}
$this->em = $args->getObjectManager();
/** @var Organization $organization */
$organization = $entity->getOrganization();
if (!is_null($organization) && isset($_SESSION['add_subscription'])
) {
try {
$this->organizationUtils->generateDocs($organization);
$this->segment->trackNewOrganization($organization, false);
$params = SubscriptionUtils::getSubscriptionDataToSegment($organization);
$documentRIB = $this->organizationUtils->getOrganizationRIB($organization);
if ($documentRIB) {
$this->segment->trackApprovedDocument($documentRIB, $params);
}
unset($_SESSION['add_subscription']);
} catch (\Exception $e) {
$this->captureSentryException($e);
}
}
if ($organization) {
/** @var SubscriptionRepository $subscriptionRepository */
$subscriptionRepository = $this->em->getRepository(Subscription::class);
$subscriptionStatus = SubscriptionUtils::isActiveSubscription($entity);
$subscriptionRepository->updateSubscriptionStatus($entity, $subscriptionStatus);
}
try {
if ($entity->haveDomiciliation() && !$organization->getIsNewDomiciliation()) {
$organization->setIsNewDomiciliation(true);
$this->em->flush();
}
} catch (\Exception $e) {
$this->captureSentryException($e);
}
try {
$entity->setStoredTotalPriceWithoutTax($entity->getTotalPriceWithoutTax());
$this->em->flush();
} catch (\Exception $e) {
// do nothing
}
}
private function isCourrier(): bool
{
$listProducts = [];
if (isset($_SESSION['products'])) {
foreach ($_SESSION['products'] as $value) {
/** @var Product $product */
$product = unserialize($value, [Product::class]);
$listProducts[] = $product->getUniqueKey();
}
unset($_SESSION['products']);
}
return 1 === count($listProducts) && ProductKeyEnum::SCAN_ENVELOPPE === $listProducts[0];
}
private function checkIfStoreAdditionHaveTheGoodPrice(Subscription $subscription): void
{
$organization = $subscription->getOrganization();
if (null !== $organization) {
$store = $organization->getStore();
$storeAdditionService = $subscription->getServiceStoreAddition();
if (null !== $store && null !== $storeAdditionService) {
$productStoreAddition = $this->em->getRepository(Product::class)->findStoreAdditionForStoreName($store->getName());
if (null !== $productStoreAddition) {
$priceWithoutTax = $productStoreAddition->getPriceWithoutTax();
if (null !== $priceWithoutTax) {
$storeAdditionService->setPriceWithoutTax($priceWithoutTax);
$this->entityManager->persist($storeAdditionService);
} else {
$this->sendSentryMessage(self::SERVICE_NAME.'Product store addition price not found');
}
} else {
$this->sendSentryMessage(self::SERVICE_NAME.'Product store addition not found');
}
}
}
}
}