src/Aqarmap/Bundle/ListingBundle/EventListener/ListingStatusListener.php line 330

  1. <?php
  2. namespace Aqarmap\Bundle\ListingBundle\EventListener;
  3. use App\Message\Listing\AfterChangeStatusMessage;
  4. use Aqarmap\Bundle\CreditBundle\Constant\CreditStatus;
  5. use Aqarmap\Bundle\CreditBundle\Contract\CreditManagerInterface;
  6. use Aqarmap\Bundle\CreditBundle\Entity\Credit;
  7. use Aqarmap\Bundle\ListingBundle\Constant\ListingActivityType;
  8. use Aqarmap\Bundle\ListingBundle\Constant\ListingCategories;
  9. use Aqarmap\Bundle\ListingBundle\Constant\ListingFeatures;
  10. use Aqarmap\Bundle\ListingBundle\Constant\ListingStatus;
  11. use Aqarmap\Bundle\ListingBundle\Document\ListingActivityLog;
  12. use Aqarmap\Bundle\ListingBundle\Entity\Listing;
  13. use Aqarmap\Bundle\ListingBundle\Event\ListingEvent;
  14. use Aqarmap\Bundle\ListingBundle\Message\ListingDeleted;
  15. use Aqarmap\Bundle\ListingBundle\Service\ListingManager;
  16. use Aqarmap\Bundle\ListingBundle\Service\ListingRuleMatcher;
  17. use Aqarmap\Bundle\ListingBundle\Service\SpecialAddListingService;
  18. use Aqarmap\Bundle\MainBundle\Adapter\MailerServiceInterface;
  19. use Aqarmap\Bundle\MainBundle\Constant\ActivityType;
  20. use Aqarmap\Bundle\MainBundle\Constant\Locales;
  21. use Aqarmap\Bundle\MainBundle\Helpers\MailerHelper;
  22. use Aqarmap\Bundle\MainBundle\Service\ActivityLogger;
  23. use Aqarmap\Bundle\NotificationBundle\Types\ListingHasExpired;
  24. use Aqarmap\Bundle\UserBundle\Constant\UserTypes;
  25. use Aqarmap\Bundle\UserBundle\Entity\User;
  26. use Doctrine\Common\Collections\ArrayCollection;
  27. use Doctrine\ODM\MongoDB\DocumentManager;
  28. use Doctrine\ORM\EntityManagerInterface;
  29. use Symfony\Component\DependencyInjection\ParameterBag\ParameterBagInterface;
  30. use Symfony\Component\EventDispatcher\EventSubscriberInterface;
  31. use Symfony\Component\Messenger\MessageBusInterface;
  32. use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface;
  33. use Symfony\Contracts\EventDispatcher\EventDispatcherInterface;
  34. use Symfony\Contracts\Translation\TranslatorInterface;
  35. use Twig\Environment;
  36. class ListingStatusListener implements EventSubscriberInterface
  37. {
  38. /**
  39. * @var MailerServiceInterface
  40. */
  41. protected $mailer;
  42. /**
  43. * Constructor.
  44. */
  45. public function __construct(
  46. MailerServiceInterface $mailer,
  47. private readonly EntityManagerInterface $entityManager,
  48. private readonly TokenStorageInterface $tokenStorage,
  49. private readonly ListingRuleMatcher $listingRuleMatcher,
  50. private readonly CreditManagerInterface $creditManager,
  51. private readonly ActivityLogger $activityLogger,
  52. private readonly EventDispatcherInterface $eventDispatcher,
  53. private readonly TranslatorInterface $translator,
  54. private readonly ListingManager $listingManager,
  55. private readonly Environment $engine,
  56. private readonly MailerHelper $mailerHelper,
  57. private readonly ParameterBagInterface $parameterBag,
  58. private readonly SpecialAddListingService $specialAddListingService,
  59. private readonly MessageBusInterface $messageBus,
  60. private readonly DocumentManager $documentManager,
  61. ) {
  62. $this->mailer = $mailer;
  63. }
  64. public function preSaveListingEvent(ListingEvent $event): void
  65. {
  66. $listing = $event->getListing();
  67. if (null === $listing->getStatus()) {
  68. $this->getListingManager()->changeStatus($listing, ListingStatus::DRAFT, false);
  69. }
  70. }
  71. public function onSubmittedEvent(ListingEvent $event): void
  72. {
  73. $listing = $event->getListing();
  74. // Add or edit status
  75. $isDraftOrLive = \in_array($listing->getStatus(), [
  76. ListingStatus::DRAFT,
  77. ListingStatus::LIVE,
  78. ListingStatus::REJECTED,
  79. ]);
  80. // Checking the title to prevent in-complete listings (multi-steps form) from being approved
  81. if ($isDraftOrLive && !empty($listing->getTitle())) {
  82. $this->getListingManager()->changeStatus($listing, ListingStatus::PENDING, true);
  83. }
  84. if ($this->specialAddListingService->hasSpecialAddListingGroup($listing->getUser())
  85. && ListingStatus::PENDING_PAYMENT === $listing->getStatus()
  86. && $listing->getSpecialPublicationCredit()
  87. ) {
  88. $this->getListingManager()->changeStatus($listing, ListingStatus::PENDING, true);
  89. }
  90. }
  91. public function onListingPublishEvent(ListingEvent $event): void
  92. {
  93. $listing = $event->getListing();
  94. $user = $listing->getUser();
  95. // unsetting pending payment and photos statuses
  96. $listing->setPendingPaymentStatus(null);
  97. $listing->setPendingPhotosStatus(null);
  98. // If no publish did already or the listing is expired and being relisted give it a bump
  99. if (!$listing->getPublishedAt() || ListingStatus::EXPIRED == $listing->getStatus()) {
  100. $listing->setPublishedAt(new \DateTime());
  101. }
  102. $em = $this->entityManager;
  103. $userListingsCount = $em->getRepository(Listing::class)
  104. ->getUserListingsCountByStatus($user, [ListingStatus::LIVE]);
  105. $token = $this->tokenStorage->getToken();
  106. // Only flag if user has more than 3 listings and he is not already flagged
  107. if ($token
  108. && !$user->hasRole('ROLE_IN_HOUSE')
  109. && $userListingsCount >= 2
  110. && UserTypes::INDIVIDUAL == $user->getUserType()
  111. ) {
  112. $user->setUserType(UserTypes::BROKER);
  113. $em->persist($user);
  114. $em->flush($user);
  115. }
  116. $listing->addRejections(new ArrayCollection());
  117. }
  118. public function onListingPublishForFreeEvent(ListingEvent $event): void
  119. {
  120. $listing = $event->getListing();
  121. $listingRules = $this->listingRuleMatcher->match($listing);
  122. if ($listingRules['publication_fees']) {
  123. $creditManager = $this->creditManager;
  124. $credits = $creditManager->deduction($listing->getUser(), (float) 0, 'Free publishing', CreditStatus::SUCCESS);
  125. foreach ($credits as $credit) {
  126. if ($credit instanceof Credit) {
  127. $this->getListingManager()->addFeature($listing, ListingFeatures::PAID, null, $credit);
  128. }
  129. }
  130. // Record this action in the activity logger with the acting user
  131. if (ListingCategories::SCRAPPED != $listing->getCategory()) {
  132. $actor = $this->tokenStorage->getToken()?->getUser();
  133. if (!$actor instanceof User) {
  134. $actor = $listing->getUser();
  135. }
  136. $this->activityLogger->record(ActivityType::LISTING_FREE_PUBLISH, $actor, $listing->getId());
  137. }
  138. }
  139. }
  140. public function onPublishedForFreeEmailEvent(ListingEvent $event): void
  141. {
  142. $listing = $event->getListing();
  143. $originalLocale = $this->getTranslator()->getLocale();
  144. $this->getTranslator()->setLocale($this->getListingLanguage($listing));
  145. $template = '@AqarmapListingBundle/Admin/Email/listingApprovedForFree.html.twig';
  146. $templateContext = ['listing' => $listing];
  147. $compose = $this->getComposeMessage($listing, $template, $templateContext, 'listings-free-publish');
  148. $this->getMailer()->sendMessage($compose);
  149. $this->getTranslator()->setLocale($originalLocale);
  150. }
  151. /**
  152. * Unset DeletedAt date on undelete event.
  153. */
  154. public function onListingUndeleteEvent(ListingEvent $event): void
  155. {
  156. $listing = $event->getListing();
  157. $listing->setDeletedAt(null);
  158. }
  159. /**
  160. * Refund listing credit on delete event.
  161. */
  162. public function onListingDeleteEvent(ListingEvent $event): void
  163. {
  164. $listing = $event->getListing();
  165. $listingFeatured = $listing->getNotExpiredCredit();
  166. /*
  167. Refund Policy,
  168. listings should have paid features that are not expired yet,
  169. Also the listing should never been published before.
  170. */
  171. if (!empty($listingFeatured) && !$listing->getPublishedAt()) {
  172. foreach ((array) $listingFeatured as $listingFees) {
  173. $credits[] = $listingFees->getCredit();
  174. }
  175. $creditManager = $this->creditManager;
  176. $em = $this->getEntityManager();
  177. foreach ($credits as $credit) {
  178. if ($credit->canBeCancelled()) {
  179. $creditManager->deposit($credit->getUser(), -$credit->getAmount(), CreditStatus::SUCCESS, CreditStatus::REFUND_LABEL);
  180. $credit->setStatus(CreditStatus::REFUND);
  181. $em->persist($credit);
  182. }
  183. }
  184. $em->flush();
  185. }
  186. $listing->setDeletedAt(new \DateTime());
  187. }
  188. /**
  189. * Refund listing credit on delete event.
  190. */
  191. public function onListingStatusDeleteEvent(ListingEvent $event): void
  192. {
  193. $this->messageBus->dispatch(new ListingDeleted($event->getListing()));
  194. }
  195. public function onApprovedSendEmailEvent(ListingEvent $event): void
  196. {
  197. $listing = $event->getListing();
  198. $originalLocale = $this->getTranslator()->getLocale();
  199. $this->getTranslator()->setLocale($this->getListingLanguage($listing));
  200. $template = '@AqarmapListingBundle/Admin/Email/listingApproved.html.twig';
  201. $templateContext = ['listing' => $listing];
  202. $compose = $this->getComposeMessage($listing, $template, $templateContext, 'listings-approval');
  203. $this->getMailer()->sendMessage($compose);
  204. $this->getTranslator()->setLocale($originalLocale);
  205. }
  206. public function onListingExpiredEvent(ListingEvent $event): void
  207. {
  208. $listing = $event->getListing();
  209. $this->eventDispatcher
  210. ->dispatch((new ListingHasExpired())->setSubject($listing), 'listing.has.expired');
  211. }
  212. public function onListingPhotosDeleted(ListingEvent $event): void
  213. {
  214. $listing = $event->getListing();
  215. $this->getListingManager()->changeStatus($listing, ListingStatus::PENDING_PHOTOS, true);
  216. }
  217. /**
  218. * @return object
  219. */
  220. public function getEntityManager()
  221. {
  222. return $this->entityManager;
  223. }
  224. /**
  225. * @return MailerServiceInterface
  226. */
  227. public function getMailer()
  228. {
  229. return $this->mailer;
  230. }
  231. /**
  232. * @return \Symfony\Component\Translation\LoggingTranslator
  233. */
  234. public function getTranslator()
  235. {
  236. return $this->translator;
  237. }
  238. public function getTemplating()
  239. {
  240. return $this->engine;
  241. }
  242. public function changeListingWaitingTime(ListingEvent $event): void
  243. {
  244. $now = new \DateTime();
  245. $listing = $event->getListing();
  246. $activityRepo = $this->documentManager->getRepository(ListingActivityLog::class);
  247. $lastActivityWaitingTime = $activityRepo->getLastActivity($listing->getId(), ListingActivityType::PENDING_APPROVAL);
  248. $listing->setWaitingTime($lastActivityWaitingTime ? $now->getTimestamp() - $lastActivityWaitingTime->getCreatedAt()->getTimestamp() : null);
  249. $this->entityManager->persist($listing);
  250. $this->entityManager->flush();
  251. }
  252. public function onListingStatusPendingEvent(ListingEvent $event): void
  253. {
  254. $listing = $event->getListing();
  255. if (!$listing->getPendingStatusCreatedAt() && !$event->getIsAdmin()) {
  256. $listing->setPendingStatusCreatedAt(new \DateTime());
  257. }
  258. $listing->setAutoReviewedAt(null);
  259. }
  260. public function onListingStatusChangedEvent(ListingEvent $event): void
  261. {
  262. $listing = $event->getListing();
  263. if ($listing->getId()) {
  264. $this->messageBus->dispatch(new AfterChangeStatusMessage($listing->getId()));
  265. }
  266. }
  267. public static function getSubscribedEvents(): array
  268. {
  269. return [
  270. 'aqarmap.listing.pre_save' => ['preSaveListingEvent'],
  271. 'aqarmap.listing.publish' => [
  272. ['onListingPublishEvent', 100], ['changeListingWaitingTime'],
  273. ],
  274. 'aqarmap.listing.submitted' => ['onSubmittedEvent', 100],
  275. 'aqarmap.listing.expired' => ['onListingExpiredEvent'],
  276. 'aqarmap.listing.undelete' => ['onListingUndeleteEvent'],
  277. 'aqarmap.listing.delete' => [['onListingStatusDeleteEvent'], ['onListingDeleteEvent']],
  278. 'aqarmap.listing.delete_by_user' => ['onListingStatusDeleteEvent'],
  279. 'aqarmap.listing.free_publish' => [
  280. ['onListingPublishForFreeEvent'], ['onListingPublishEvent'], ['onPublishedForFreeEmailEvent'],
  281. ],
  282. 'aqarmap.listing.publish_without_photos' => [
  283. ['onListingPublishForFreeEvent'], ['onListingPublishEvent'],
  284. ],
  285. 'aqarmap.listing.photos_deleted' => ['onListingPhotosDeleted'],
  286. 'aqarmap.listing.rejected' => ['changeListingWaitingTime'],
  287. 'aqarmap.listing.pending_approval' => ['onListingStatusPendingEvent'],
  288. 'aqarmap.listing.after_change_status' => ['onListingStatusChangedEvent'],
  289. ];
  290. }
  291. /**
  292. * @return ListingManager
  293. */
  294. protected function getListingManager()
  295. {
  296. return $this->listingManager;
  297. }
  298. private function getComposeMessage($listing, $template, $templateContext, $headerCategory)
  299. {
  300. $composeMessage = $this->mailerHelper->createMessageWithGlobalAttributes();
  301. $composeMessage->setSubject($this->getTranslator()->trans('email.subject.listing_approved'));
  302. $composeMessage->setTo($listing->getUser()->getEmailCanonical());
  303. $composeMessage->setReplyTo($listing->getUser()->getEmailCanonical());
  304. $composeMessage->setTemplate($template);
  305. $composeMessage->setTemplateContext($templateContext);
  306. $compose = $this->getMailer()->composeMessage($composeMessage);
  307. $compose->getHeaders()->addTextHeader('X-Mail-Category', $headerCategory);
  308. $compose->getHeaders()->addTextHeader('X-Site-Country', $this->parameterBag->get('country'));
  309. return $compose;
  310. }
  311. private function getListingLanguage($listing)
  312. {
  313. $language = Locales::AR;
  314. if (null !== $listing->getUser()->getLanguage()) {
  315. $language = $listing->getUser()->getLanguage();
  316. }
  317. return $language;
  318. }
  319. }