src/Aqarmap/Bundle/ListingBundle/Controller/ListingController.php line 771

  1. <?php
  2. namespace Aqarmap\Bundle\ListingBundle\Controller;
  3. use App\Exception\LogicHttpException;
  4. use App\Security\Voter\SubAccountPermissionVoter;
  5. use App\Service\User\SubAccountService;
  6. use Aqarmap\Bundle\CreditBundle\Constant\CreditStatus;
  7. use Aqarmap\Bundle\CreditBundle\Entity\Credit;
  8. use Aqarmap\Bundle\CreditBundle\Services\CreditManager;
  9. use Aqarmap\Bundle\FeatureToggleBundle\Service\FeatureToggleManager;
  10. use Aqarmap\Bundle\FinancialAidsBundle\Service\FinancialAidService;
  11. use Aqarmap\Bundle\ListingBundle\Constant\LeadTypes;
  12. use Aqarmap\Bundle\ListingBundle\Constant\ListingCategories;
  13. use Aqarmap\Bundle\ListingBundle\Constant\ListingFeaturedTypes;
  14. use Aqarmap\Bundle\ListingBundle\Constant\ListingFeatures;
  15. use Aqarmap\Bundle\ListingBundle\Constant\ListingSections;
  16. use Aqarmap\Bundle\ListingBundle\Constant\ListingSource;
  17. use Aqarmap\Bundle\ListingBundle\Constant\ListingStatus;
  18. use Aqarmap\Bundle\ListingBundle\Constant\PropertyRegistrationStatusOption;
  19. use Aqarmap\Bundle\ListingBundle\Constant\ResaleListingsConstant;
  20. use Aqarmap\Bundle\ListingBundle\Contracts\PhoneManagerInterface;
  21. use Aqarmap\Bundle\ListingBundle\Contracts\RelatedResultServiceInterface;
  22. use Aqarmap\Bundle\ListingBundle\Entity\CallRequest;
  23. use Aqarmap\Bundle\ListingBundle\Entity\File;
  24. use Aqarmap\Bundle\ListingBundle\Entity\Listing;
  25. use Aqarmap\Bundle\ListingBundle\Entity\ListingPhone;
  26. use Aqarmap\Bundle\ListingBundle\Entity\Phone;
  27. use Aqarmap\Bundle\ListingBundle\Event\ListingEvent;
  28. use Aqarmap\Bundle\ListingBundle\Exception\InvalidLeadValidationHttpException;
  29. use Aqarmap\Bundle\ListingBundle\Form\CallRequestFormType;
  30. use Aqarmap\Bundle\ListingBundle\Form\ContactSellerFormType;
  31. use Aqarmap\Bundle\ListingBundle\Form\ContactSellerWideFormType;
  32. use Aqarmap\Bundle\ListingBundle\Form\ListingInitializeType;
  33. use Aqarmap\Bundle\ListingBundle\Form\ListingType;
  34. use Aqarmap\Bundle\ListingBundle\Form\PhotoType;
  35. use Aqarmap\Bundle\ListingBundle\Form\QuickLeadType;
  36. use Aqarmap\Bundle\ListingBundle\Repository\ListingPhotoRepository;
  37. use Aqarmap\Bundle\ListingBundle\Service\CallRequestManager;
  38. use Aqarmap\Bundle\ListingBundle\Service\Contracts\ListingLeadManagerInterface;
  39. use Aqarmap\Bundle\ListingBundle\Service\Contracts\LocationManagerInterface;
  40. use Aqarmap\Bundle\ListingBundle\Service\FreeListingService;
  41. use Aqarmap\Bundle\ListingBundle\Service\InteractionService;
  42. use Aqarmap\Bundle\ListingBundle\Service\ListingFeatureService;
  43. use Aqarmap\Bundle\ListingBundle\Service\ListingManager;
  44. use Aqarmap\Bundle\ListingBundle\Service\ListingRuleMatcher;
  45. use Aqarmap\Bundle\ListingBundle\Service\Mortgage\MortgageService;
  46. use Aqarmap\Bundle\ListingBundle\Service\V4\CompoundDetailService;
  47. use Aqarmap\Bundle\MainBundle\Form\ConfirmFeaturedFormType;
  48. use Aqarmap\Bundle\MainBundle\Form\ConfirmFormType;
  49. use Aqarmap\Bundle\MainBundle\Service\MobileDetectionService;
  50. use Aqarmap\Bundle\MainBundle\Service\Setting;
  51. use Aqarmap\Bundle\MessageBundle\Service\Composer;
  52. use Aqarmap\Bundle\NeighborhoodBundle\Service\NeighborhoodManager;
  53. use Aqarmap\Bundle\NotificationBundle\DatabaseNotification;
  54. use Aqarmap\Bundle\NotifierBundle\Event\NotifierEvent;
  55. use Aqarmap\Bundle\OTPBundle\Contract\OtpServiceInterface;
  56. use Aqarmap\Bundle\SearchBundle\Services\CompoundFaqsService;
  57. use Aqarmap\Bundle\SearchBundle\Services\ElasticListingSearch\DefaultListingSearch;
  58. use Aqarmap\Bundle\TopSellerBundle\Model\TopSeller;
  59. use Aqarmap\Bundle\TopSellerBundle\Service\TopSellerRetrievalService;
  60. use Aqarmap\Bundle\UserBundle\Constant\UserServicesType;
  61. use Aqarmap\Bundle\UserBundle\Constant\UserTypes;
  62. use Aqarmap\Bundle\UserBundle\Entity\User;
  63. use Aqarmap\Bundle\UserBundle\Entity\UserServices;
  64. use Aqarmap\Bundle\UserBundle\Form\QuickRegistrationFormType;
  65. use Aqarmap\Bundle\UserBundle\Repository\UserPackagesRepository;
  66. use Aqarmap\Bundle\UserBundle\Services\UserActivityService;
  67. use Aqarmap\Bundle\UserBundle\Services\UserManager;
  68. use Aqarmap\Bundle\UserBundle\Services\UserPackagesService;
  69. use Aqarmap\Bundle\UserBundle\Services\UserServicesManager;
  70. use Doctrine\ORM\EntityManager;
  71. use Doctrine\ORM\EntityManagerInterface;
  72. use FOS\RestBundle\Controller\Annotations as Rest;
  73. use FOS\RestBundle\View\View;
  74. use FOS\UserBundle\Model\UserManagerInterface;
  75. use Gedmo\Translatable\TranslatableListener;
  76. use Knp\Component\Pager\PaginatorInterface;
  77. use Psr\Log\LoggerInterface;
  78. use Psr\Log\LogLevel;
  79. use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
  80. use Symfony\Component\EventDispatcher\EventDispatcherInterface;
  81. use Symfony\Component\ExpressionLanguage\Expression;
  82. use Symfony\Component\HttpFoundation\RedirectResponse;
  83. use Symfony\Component\HttpFoundation\Request;
  84. use Symfony\Component\HttpFoundation\Response;
  85. use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
  86. use Symfony\Component\Routing\Attribute\Route;
  87. use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface;
  88. use Symfony\Component\Security\Core\Authorization\AuthorizationCheckerInterface;
  89. use Symfony\Component\Security\Http\Attribute\IsGranted;
  90. use Symfony\Contracts\Translation\TranslatorInterface;
  91. use Vich\UploaderBundle\Handler\DownloadHandler;
  92. /**
  93. * Listing controller.
  94. */
  95. class ListingController extends AbstractController
  96. {
  97. public function __construct(
  98. private readonly Composer $messageComposer,
  99. OtpServiceInterface $otpService,
  100. private readonly DatabaseNotification $databaseNotification,
  101. private readonly LoggerInterface $logger,
  102. private readonly TranslatorInterface $translator,
  103. private readonly FeatureToggleManager $featureToggleManager,
  104. private readonly Setting $setting,
  105. private readonly ListingManager $listingManager,
  106. private readonly EventDispatcherInterface $dispatcher,
  107. private readonly InteractionService $interactionService,
  108. private readonly NeighborhoodManager $neighborhoodManager,
  109. private readonly DefaultListingSearch $defaultListingSearch,
  110. private readonly RelatedResultServiceInterface $relatedResultService,
  111. LocationManagerInterface $locationManager,
  112. private readonly TopSellerRetrievalService $topSellerRetrievalService,
  113. private readonly FinancialAidService $financialAidService,
  114. private readonly ListingLeadManagerInterface $listingLeadManager,
  115. private readonly CompoundFaqsService $compoundFaqsService,
  116. private readonly PaginatorInterface $paginator,
  117. private readonly UserActivityService $userActivityService,
  118. private readonly MobileDetectionService $mobileDetectionService,
  119. private readonly CallRequestManager $callRequestManager,
  120. private readonly AuthorizationCheckerInterface $authorizationChecker,
  121. TokenStorageInterface $tokenStorage,
  122. private readonly MortgageService $mortgageService,
  123. private readonly ListingRuleMatcher $listingRuleMatcher,
  124. private readonly CreditManager $creditManager,
  125. private readonly UserServicesManager $userServicesManager,
  126. private readonly UserManagerInterface $FOSUserManager,
  127. private readonly PhoneManagerInterface $phoneManager,
  128. FreeListingService $freeListingService,
  129. private readonly ListingFeatureService $listingFeatureService,
  130. private readonly UserManager $userManager,
  131. private readonly CompoundDetailService $compoundDetailService,
  132. private readonly TranslatableListener $translatableListener,
  133. private readonly UserPackagesRepository $userPackagesRepository,
  134. private readonly \Doctrine\Persistence\ManagerRegistry $managerRegistry,
  135. private readonly SubAccountService $subAccountService,
  136. ) {
  137. }
  138. /**
  139. * Listing Details Action.
  140. */
  141. #[Route(path: '/{id}/{notification}', name: 'listing_view', requirements: ['id' => '\d+'], options: ['i18n' => false, 'expose' => true], defaults: ['notification' => 0], methods: ['GET'])]
  142. #[Route(path: '/listing/{id}/notification/{notification}', name: 'listing_view_notification', defaults: ['notification' => 0], methods: ['GET'])]
  143. #[Route(path: '/listing/{id}', name: 'listing_details', requirements: ['id' => '\d+'], options: ['expose' => true], methods: ['GET'])]
  144. #[Route(path: '/listing/{id}-{slug}', name: 'listing_slug', requirements: ['id' => '\d+', 'slug' => '.+'], methods: ['GET'])]
  145. public function read(Request $request, Listing $listing, EntityManagerInterface $em, UserServicesManager $userServicesManager)
  146. {
  147. if ($request->get('notification')) {
  148. try {
  149. $this->databaseNotification->markOneAsRead($request->get('notification'));
  150. } catch (\Exception $exception) {
  151. $this->logger->log(LogLevel::ERROR, $exception->getMessage());
  152. }
  153. }
  154. // Redirect to the right URL if the URL is short-URL or the listing slug is incorrect
  155. if (('listing_slug' != $request->get('_route')
  156. || $request->attributes->get('slug') != $listing->getSlug())
  157. && !empty($listing->getSlug())
  158. ) {
  159. return $this->redirectToRoute('listing_slug', array_merge($request->query->all(), [
  160. 'id' => $listing->getId(),
  161. 'slug' => $listing->getSlug(),
  162. ]), Response::HTTP_MOVED_PERMANENTLY);
  163. }
  164. $listingStatus = !\in_array($listing->getStatus(), [ListingStatus::LIVE, ListingStatus::PENDING]);
  165. if ($listingStatus && !$request->query->get('noredirect') && $listing->getUser() != $this->getUser()) {
  166. if (!$listing->getSection()->getSearchable()) {
  167. return $this->redirectToRoute('compound_search', [], Response::HTTP_FOUND);
  168. }
  169. try {
  170. $searchableLocation = $listing->getLocation()->getNearestSearchable();
  171. } catch (\Exception) {
  172. $this->addFlash(
  173. 'danger',
  174. $this->translator->trans('listing.not_available')
  175. );
  176. return $this->redirectToRoute('homepage', [], Response::HTTP_FOUND);
  177. }
  178. return $this->redirectToRoute('search', [
  179. 'section_slug' => $listing->getSection()->getSlug(),
  180. 'property_type_slug' => $listing->getPropertyType()->getSlug(),
  181. 'location_slug' => $searchableLocation->getSlug(),
  182. ], Response::HTTP_MOVED_PERMANENTLY);
  183. }
  184. $listingRepo = $em->getRepository(Listing::class);
  185. if ($notifierId = $request->query->get('notifier')) {
  186. if (null !== $notifier = $em->getRepository(\Aqarmap\Bundle\NotifierBundle\Entity\Notifier::class)->find($notifierId)) {
  187. $this->dispatcher->dispatch(new NotifierEvent($notifier), 'aqarmap.notifier.interaction');
  188. } else {
  189. $this->logger->warning('Notifier with id: '.$notifierId.' was not found!');
  190. }
  191. }
  192. $this->interactionService->increaseViews($listing, $this->getUser());
  193. $relatedListingCriteriaMapper = $this->relatedResultService->getRelatedListingCriteriaMapper($listing);
  194. $relatedListings = $this->defaultListingSearch->search($relatedListingCriteriaMapper)['searchResults'];
  195. $topSeller = $this->createTopSeller($listing);
  196. $topSearchableCompanies = $this->topSellerRetrievalService->getTopSellerPersonalData($topSeller, $request->getLocale());
  197. $searchableLocation = $listing->getLocation()->getNearestSearchable();
  198. $isPlaceHoldered = false;
  199. $userProfilePhoto = $listing->getUser()->isValidPersonalPhoto() ? $listing->getUser()->getPersonalPhoto() : null;
  200. if ($listing->getLogo() || ($listing->getParent() && $listing->getParent()->getLogo())) {
  201. if ($listing->getParent() && $listing->getParent()->getLogo()) {
  202. }
  203. } elseif (!empty($listing->getPhotosForSlider()) && $listing->getUser()->getIsValidLogo()) {
  204. $isPlaceHoldered = true;
  205. }
  206. $this->financialAidService->setFinancialAidInListing($listing);
  207. $activeListingsCount = $listing->getUser()->getActiveListingsCount();
  208. $leadsCount = 0;
  209. if ($this->featureToggleManager->isEnabled('web.client.served.count')) {
  210. $leadsCount = $listing->getUser()->getClientServedCount();
  211. }
  212. $listing = current(
  213. $this->listingManager->setUserActivities(
  214. [$listing],
  215. [$listing->getId()]
  216. )
  217. );
  218. $hasUserMadeLead = false;
  219. if ($user = $this->getUser()) {
  220. $hasUserMadeLead = $this->listingLeadManager->hasUserMadeLead($listing, $user);
  221. }
  222. $compoundFaqs = [];
  223. $listingDetails = $listing;
  224. if ($listing->isProject()) {
  225. $compoundFaqs = $this->compoundFaqsService->generateFaqData($listing);
  226. $resaleRegularListingsPaginated = $this->paginator->paginate(
  227. $listingRepo->getSimilarListingsInSection($listing, ListingSections::FOR_SALE),
  228. max($request->query->getInt('resalePage', ResaleListingsConstant::PAGE), ResaleListingsConstant::PAGE),
  229. ResaleListingsConstant::LIMIT,
  230. [
  231. 'pageParameterName' => 'resalePage',
  232. 'sortFieldParameterName' => 'resaleSort',
  233. 'sortDirectionParameterName' => 'resaleDirection',
  234. ]
  235. );
  236. $this->compoundDetailService->getPropertyTypeChildrens($listing, $request->getLocale());
  237. $liveUnitsPaginated = $listing->getPropertyTypeChildren();
  238. $this->compoundDetailService->getPropertyTypeUnitsChildrens($listing, ListingSections::FOR_SALE, $request->getLocale());
  239. $resaleUnitsPaginated = $listing->getPropertyTypeChildren();
  240. $this->compoundDetailService->getPropertyTypeUnitsChildrens($listing, ListingSections::FOR_RENT, $request->getLocale());
  241. $rentUnitsPaginated = $listing->getPropertyTypeChildren();
  242. }
  243. $request->cookies->get('user-agent');
  244. $mobileDetection = $this->mobileDetectionService->mobileDetection($request, $return);
  245. $return = [
  246. 'listing' => $listingDetails,
  247. 'searchableLocation' => $searchableLocation,
  248. 'contact_seller_form' => $this->contactSellerForm($listing)->createView(),
  249. 'quick_registration_form' => $this->quickRegistrationForm()->createView(),
  250. 'call_request' => $this->callRequestForm($listing)->createView(),
  251. 'related_listings' => $relatedListings,
  252. 'relatedListingsCount' => (null != $relatedListings) ? $relatedListings->getTotalItemCount() : 0,
  253. 'location_statistics' => $this->neighborhoodManager->getStatistics(
  254. $listing->getLocation()->getNearestNeighborhood(),
  255. $listing->getPropertyType()
  256. ),
  257. 'topSearchableCompanies' => $topSearchableCompanies,
  258. 'topSearchableCompaniesCount' => \count($topSearchableCompanies),
  259. 'isPlaceHoldered' => $isPlaceHoldered,
  260. 'userProfilePhoto' => $userProfilePhoto,
  261. 'activeListingsCount' => $activeListingsCount,
  262. 'leadsCount' => $leadsCount,
  263. 'featureToggle' => $this->userActivityService->getFeatureToggles(),
  264. 'leadAnalytics' => $this->listingManager->getLeadAnalytics($listing),
  265. 'otherUnits' => $this->listingManager->getOtherUnits($listing, $request->getLocale()),
  266. 'hasUserMadeLead' => $hasUserMadeLead,
  267. 'resaleRegularListings' => $resaleRegularListingsPaginated ?? null,
  268. 'resaleUnitsPaginated' => $resaleUnitsPaginated ?? null,
  269. 'liveUnitsPaginated' => $liveUnitsPaginated ?? null,
  270. 'rentUnitsPaginated' => $rentUnitsPaginated ?? null,
  271. 'compoundFaqs' => $compoundFaqs,
  272. 'isMobile' => $mobileDetection['isMobile'],
  273. 'hasCompoundRatingService' => $user ? $userServicesManager->hasActiveService(
  274. UserServicesType::COMPOUND_RATING,
  275. $user->getId()
  276. ) : false,
  277. ];
  278. if ($mobileDetection['isMobile']) {
  279. $nearestLocations = $em
  280. ->getRepository(\Aqarmap\Bundle\ListingBundle\Entity\Location::class)
  281. ->getNearestLocations([$listing->getLocation()]);
  282. $return['nearestLocations'] = $nearestLocations;
  283. foreach ($mobileDetection as $key => $val) {
  284. $return[$key] = $val;
  285. }
  286. return $this->render('@AqarmapListing/Listing/read-mob.html.twig', $return);
  287. }
  288. $return['discussions'] = $em
  289. ->getRepository(\Aqarmap\Bundle\DiscussionBundle\Entity\Discussion::class)
  290. ->getTrendingWithLocation($listing->getLocation()->getId(), 3);
  291. return $this->render('@AqarmapListing/Listing/read.html.twig', $return);
  292. }
  293. /**
  294. * TopSeller Attributes from listing data.
  295. *
  296. * @param Listing
  297. *
  298. * @return TopSeller
  299. */
  300. private function createTopSeller($listing)
  301. {
  302. $topSeller = new TopSeller();
  303. $topSeller->setLocation($listing->getLocation()->getId());
  304. $topSeller->setSection($listing->getSection()->getId());
  305. $topSeller->setPropertyType($listing->getPropertyType()->getId());
  306. return $topSeller;
  307. }
  308. /**
  309. * Latest Listings Action.
  310. */
  311. #[Route(path: '/listing/latest', name: 'listing_latest', methods: ['GET'])]
  312. public function latest(Request $request): Response
  313. {
  314. /** @var EntityManager $em */
  315. $em = $this->managerRegistry->getManager();
  316. $pagination = $this->paginator->paginate(
  317. $em->getRepository(Listing::class)->getLatestListings(),
  318. $request->query->getInt('page', 1),
  319. 25
  320. );
  321. return $this->render('@AqarmapListing/Listing/latest.html.twig', [
  322. 'listings' => $pagination,
  323. ]);
  324. }
  325. /**
  326. * Add Listing First Step Action.
  327. */
  328. #[Route(path: '/listing/initialize', name: 'listing_initialize', options: ['expose' => true])]
  329. public function initialize(Request $request)
  330. {
  331. $user = $this->getUser();
  332. if (false === $this->authorizationChecker->isGranted('IS_AUTHENTICATED_REMEMBERED')) {
  333. return $this->redirectToRoute('aqarmap_add_listing');
  334. }
  335. $this->denyAccessUnlessGranted(SubAccountPermissionVoter::MANAGE_LISTINGS);
  336. $owner = $user instanceof User ? $this->subAccountService->resolveListingOwner($user) : $user;
  337. $isPhoneVerified = true;
  338. if ($owner instanceof User) {
  339. $isPhoneVerified = $owner->isPhoneVerified();
  340. }
  341. $hasRentalPackage = $this->userPackagesRepository->hasRentalPackage($user);
  342. $initializeOptions = [
  343. 'action' => $this->generateUrl('listing_initialize'),
  344. 'method' => 'POST',
  345. 'em' => $this->managerRegistry->getManager(),
  346. 'is_admin' => $this->authorizationChecker->isGranted('ROLE_ADMIN'),
  347. 'has_rental_package' => $hasRentalPackage,
  348. 'parentUser' => $user,
  349. ];
  350. $form = $this->createForm(ListingInitializeType::class, $listing = new Listing(), $initializeOptions);
  351. $listingEvent = new ListingEvent($listing);
  352. $this->dispatcher->dispatch($listingEvent, 'aqarmap.listing.pre_submitted');
  353. $form->handleRequest($request);
  354. if ($form->isSubmitted() && $form->isValid()) {
  355. if (!$isPhoneVerified && !$owner->getHasActiveSubscription()) {
  356. $this->addFlash('danger', $this->translator->trans('listing.notice.cannot_add_before_verify'));
  357. return $this->render('@AqarmapListing/Listing/initialize.html.twig', [
  358. 'form' => $form,
  359. 'isPhoneVerified' => $isPhoneVerified,
  360. ]);
  361. }
  362. $listing = $this->listingManager->createDraft($listing);
  363. // Add user phone number to the listing
  364. if ($listing->getUser()->getPhoneNumber()) {
  365. // link user phone with this listing
  366. // TODO: Suggest only the main number for now, later on we will suggest the main + last 2 numbers.
  367. $this->listingManager->suggestListingPhoneNumbers(
  368. [$listing->getUser()->getPhoneNumber()],
  369. $listing
  370. );
  371. }
  372. return $this->redirectToRoute('listing_edit', [
  373. 'id' => $listing->getId(),
  374. ]);
  375. }
  376. return $this->render('@AqarmapListing/Listing/initialize.html.twig', [
  377. 'form' => $form,
  378. 'isPhoneVerified' => $isPhoneVerified,
  379. ]);
  380. }
  381. /**
  382. * Listing Post Action
  383. * This action manage adding & editing listings.
  384. */
  385. #[Route(path: '/listing/{id}/edit/', name: 'listing_edit', requirements: ['id' => '\d+'], options: ['expose' => true])]
  386. #[IsGranted(attribute: new Expression('(is_granted("ROLE_USER") and is_granted("ROLE_OWNER", subject["listing"])) or is_granted("ROLE_ADMIN")'), subject: ['listing'])]
  387. public function post(Request $request, Listing $listing): Response
  388. {
  389. $user = $this->getUser();
  390. // if user doesn't have phone or active subscription, redirect to my listings with an error flash message
  391. if ($user instanceof User && (!$user->isPhoneVerified() && !$user->getHasActiveSubscription())) {
  392. $this->addFlash('danger', $this->translator->trans('listing.notice.cannot_add_before_verify'));
  393. return $this->redirectToRoute('aqarmap_listing_default_mylistings');
  394. }
  395. $form = $this->createForm(ListingType::class, $listing, [
  396. 'action' => $this->generateUrl('listing_edit', ['id' => $listing->getId()]),
  397. 'method' => 'POST',
  398. 'listingIsLiveFor5Days' => $this->listingManager->checkListingLiveDays($listing),
  399. 'is_admin' => $this->authorizationChecker->isGranted('ROLE_ADMIN'),
  400. 'user_country' => $this->setting->getSetting('general', 'country'),
  401. 'user_type' => $listing->getUser()?->getUserType(),
  402. 'isMortgageOptionsEnabled' => $this->featureToggleManager->isEnabled('web.mortgage.options'),
  403. 'isListingMarketPropertyTypesEnabled' => $this->featureToggleManager->isEnabled('web.listing.market.property.types'),
  404. ]);
  405. $form->handleRequest($request);
  406. if ($request->isMethod('POST')) {
  407. if ($form->isSubmitted() && $form->isValid()) {
  408. $this->updateWhatsAppPhones($listing->getUser()->getId(), $request->request->all('phoneIds'), $request->request->all('hasWhatsApp'));
  409. $listing = $this->handleListingPhones($listing, $request->request->all('listing')['phones'] ?? []);
  410. $this->handleUserPhones($listing->getUser(), $request->request->all('listing')['phones'] ?? []);
  411. $this->listingManager->saveListing($listing);
  412. if ($this->featureToggleManager->isEnabled('web.add.listing.translations')) {
  413. if ('en' == $request->getLocale()) {
  414. $criteria = [
  415. 'reversedLocale' => 'ar',
  416. 'title-ar' => $request->request->get('title-ar'),
  417. 'description-ar' => $request->request->get('description-ar'),
  418. ];
  419. } else {
  420. $criteria = [
  421. 'reversedLocale' => 'en',
  422. 'title-en' => $request->request->get('title-en'),
  423. 'description-en' => $request->request->get('description-en'),
  424. ];
  425. }
  426. $this->listingManager->updateListingTranslationsFields($listing, $criteria);
  427. } else {
  428. $this->listingManager->updateListingTranslations($listing);
  429. }
  430. if ($this->featureToggleManager->isEnabled('web.mortgage.options')) {
  431. $propertyRegistrationStatus = $form->has('propertyRegistrationStatus') ? $form->get('propertyRegistrationStatus')->getData() : null;
  432. if (\in_array($propertyRegistrationStatus, PropertyRegistrationStatusOption::getValidMorgageOptions())) {
  433. $this->mortgageService->addEligibleMortgageTypes($listing);
  434. } else {
  435. $this->listingManager->setELigibleMortgageToNull($listing);
  436. }
  437. $listing = $this->mortgageService->addMortgageApproval($listing, $form);
  438. }
  439. $listingEvent = new ListingEvent($listing);
  440. $this->dispatcher->dispatch($listingEvent, 'aqarmap.listing.submitted');
  441. if (
  442. ListingSource::SCRAPPING == $listing->getSource()
  443. || ListingSource::LITE == $listing->getSource()
  444. ) {
  445. return $this->redirectToRoute('listing_view', ['id' => $listing->getId()]);
  446. }
  447. return $this->redirectToRoute('listing_upload', ['id' => $listing->getId(), '_locale' => $request->get('_locale')]);
  448. }
  449. $this->addFlash('danger', $this->translator->trans('static.problem_error_message'));
  450. }
  451. $translatedLocale = 'en' == $request->getLocale() ? 'ar' : 'en';
  452. return $this->render('@AqarmapListing/Listing/post.html.twig', [
  453. 'form' => $form,
  454. 'listing' => $listing,
  455. 'title_translation' => $this->listingTitleTranslations($listing, $translatedLocale),
  456. 'description_translation' => $this->listingDescriptionTranslations($listing, $translatedLocale),
  457. 'brokerChoices' => UserTypes::getBrokerChoices(),
  458. ]);
  459. }
  460. /**
  461. * @return array|mixed
  462. */
  463. private function listingTitleTranslations(Listing $listing, string $locale)
  464. {
  465. $this->translatableListener->setTranslatableLocale($locale);
  466. $listing->setTranslatableLocale($locale);
  467. return $listing->getTitle() ?? $this->listingManager->refreshTranslationsAndGenerateListingTitle($listing, $locale);
  468. }
  469. private function listingDescriptionTranslations(Listing $listing, string $locale)
  470. {
  471. $this->translatableListener->setTranslatableLocale($locale);
  472. $listing->setTranslatableLocale($locale);
  473. return $listing->getDescription() ?? $this->listingManager->refreshTranslationsAndGenerateListingTitle($listing, $locale);
  474. }
  475. private function handleListingPhones(Listing $listing, array $phones): Listing
  476. {
  477. $listing->clearPhones();
  478. foreach ($phones as $phone) {
  479. $phoneNumber = $phone['number'];
  480. $countryCode = $phone['countryCode'];
  481. $phone = new Phone($countryCode.$phoneNumber, $countryCode);
  482. $listing->addPhone(new ListingPhone($phoneNumber, $listing, $countryCode, $phone));
  483. }
  484. return $listing;
  485. }
  486. private function handleUserPhones(User $user, array $phones): void
  487. {
  488. foreach ($phones as $phone) {
  489. $originalPhoneNumber = $phone['number'];
  490. $countryCode = $phone['countryCode'];
  491. $phoneNumber = $this->phoneManager->trimZero($originalPhoneNumber, $countryCode);
  492. $this->phoneManager->addNewUserPhone($phoneNumber, $countryCode, $user, true, false, null, $originalPhoneNumber);
  493. }
  494. }
  495. /**
  496. * Add Listing First Step Action.
  497. */
  498. #[Route(path: '/listing/{id}/edit/photos', name: 'listing_upload', requirements: ['id' => '\d+'], options: ['expose' => true])]
  499. #[IsGranted(attribute: new Expression('(is_granted("ROLE_USER") and is_granted("ROLE_OWNER", subject["listing"])) or is_granted("ROLE_ADMIN")'), subject: ['listing'])]
  500. public function upload(Request $request, Listing $listing)
  501. {
  502. $form = $this->createForm(PhotoType::class, null, [
  503. 'method' => 'POST',
  504. ]);
  505. $form->handleRequest($request);
  506. $listingRules = $this->listingRuleMatcher->match($listing);
  507. if ($form->isSubmitted() && $form->isValid() && $request->isMethod('POST')) {
  508. $listingPhotos = [];
  509. try {
  510. $listingPhotos = $this->listingManager->addListingPhotos($listing, $form->get('file')->getData());
  511. $listingEvent = new ListingEvent($listing);
  512. $this->dispatcher->dispatch($listingEvent, 'aqarmap.listing.submitted');
  513. $this->listingManager->saveListing($listing);
  514. // If not Ajax request
  515. if (!$request->isXmlHttpRequest()) {
  516. $this->logger->error('Not AJAX');
  517. return $this->redirect($request->headers->get('referer') ?: $this->generateUrl('homepage'));
  518. }
  519. } catch (\Exception $exception) {
  520. $this->logger->error($exception->getMessage());
  521. }
  522. return View::create(['files' => $listingPhotos], Response::HTTP_OK);
  523. }
  524. return $this->render('@AqarmapListing/Listing/upload.html.twig', [
  525. 'requiredPhotosCount' => $listingRules['required_photos'] ?? 0,
  526. 'listing' => $listing,
  527. 'form' => $form,
  528. ]);
  529. }
  530. /**
  531. * Should be called after photos are uploaded.
  532. */
  533. #[Route(path: '/listing/{id}/finish', name: 'listing_finish', requirements: ['id' => '\d+'])]
  534. #[IsGranted(attribute: new Expression('(is_granted("ROLE_USER") and is_granted("ROLE_OWNER", subject["listing"])) or is_granted("ROLE_ADMIN")'), subject: ['listing'])]
  535. public function finish(Listing $listing, UserPackagesService $userPackagesService): Response
  536. {
  537. $listingEvent = new ListingEvent($listing);
  538. if (!$userPackagesService->hasAccessToList($listing->getUser(), $listing)) {
  539. $this->addFlash('danger', $this->translator->trans('credit.can_not_list_in_location'));
  540. return $this->redirectToRoute('listing_upload', [
  541. 'id' => $listing->getId(),
  542. ], Response::HTTP_FOUND);
  543. }
  544. $this->dispatcher->dispatch($listingEvent, 'aqarmap.listing.submitted');
  545. $response = $listingEvent->getResponse();
  546. if (null === $response) {
  547. if (ListingStatus::PENDING === $listing->getStatus()) {
  548. $this->addFlash(
  549. 'success',
  550. $this->translator->trans('add_listing_page.success_messages.padding_review')
  551. );
  552. if ($this->featureToggleManager->isEnabled('web.delayed.review.eid.info.message')) {
  553. $this->addFlash(
  554. 'info',
  555. $this->translator->trans('add_listing_page.info_messages.eid_delayed_review')
  556. );
  557. }
  558. }
  559. $response = new RedirectResponse(
  560. $this->generateUrl(
  561. 'listing_slug',
  562. ['id' => $listing->getId(), 'slug' => $listing->getSlug()]
  563. )
  564. );
  565. }
  566. return $response;
  567. }
  568. /**
  569. * @return RedirectResponse|Response
  570. */
  571. #[Route(path: '/listing/{id}/make_it_featured', name: 'listing_confirm_featured_credit', requirements: ['id' => '\d+'], methods: ['GET'])]
  572. #[IsGranted(attribute: 'ROLE_OWNER', subject: 'listing')]
  573. public function confirmFeaturedPublishing(Listing $listing)
  574. {
  575. if (!\in_array($listing->getStatus(), [ListingStatus::PENDING, ListingStatus::LIVE])) {
  576. $this->addFlash(
  577. 'danger',
  578. $this->translator->trans('listing.featured_failure_statement.not_live_or_pending')
  579. );
  580. return $this->redirectToRoute('aqarmap_listing_default_mylistings');
  581. }
  582. if (UserTypes::INDIVIDUAL == $listing->getUser()->getUserType()) {
  583. $listingRules = $this->listingFeatureService->getFeaturedListingRules($listing, ['sold_by_owner', 'sold_by_owner_sponsored']);
  584. } else {
  585. $listingRules = $this->listingFeatureService->getFeaturedListingRules($listing, ['sponsored', 'spotlight']);
  586. }
  587. /** @var User $user */
  588. $user = $this->getUser();
  589. return $this->render(
  590. '@AqarmapListing/Listing/featuredListingCheckout.html.twig',
  591. [
  592. 'listingRules' => $listingRules,
  593. 'listingId' => $listing->getId(),
  594. 'haveFeesToFeature' => $this->listingManager->userHasFeesToFeature($user, $listing),
  595. ]
  596. );
  597. }
  598. #[Route(path: '/listing/{id}/make_it_featured', name: 'listing_confirm_publish_featured_credit_post', requirements: ['id' => '\d+'], options: ['expose' => true], methods: ['POST'])]
  599. #[IsGranted(attribute: 'ROLE_OWNER', subject: 'listing')]
  600. public function prepareFeaturedListingPayment(Listing $listing, Request $request): RedirectResponse
  601. {
  602. $listingRules = $this->listingManager->getListingRules($listing);
  603. $fees = $request->query->get('fees', $listingRules['featured_fees']);
  604. $duration = $request->query->get('duration', $listingRules['featured_duration']);
  605. $listingFeaturedType = $request->query->get('listingFeaturedType', ListingFeaturedTypes::FEATURED);
  606. $listingFeature = $request->query->get('listingFeature', ListingFeatures::FEATURED);
  607. $listingStatus = $listing->getStatus();
  608. /** @var User $user */
  609. $user = $this->getUser();
  610. if (!$this->listingManager->isAffordable($user, $fees)) {
  611. $this->addFlash('danger', $this->translator->trans('credit.not_enough_featuring_credit', [
  612. 'link' => $this->generateUrl('aqarmap_buy_credit', ['listing_id' => $listing->getId()]),
  613. ]));
  614. return $this->redirectToRoute('listing_confirm_featured_credit', [
  615. 'id' => $listing->getId(),
  616. ], Response::HTTP_FOUND);
  617. }
  618. $featuredText = $this->translator->trans(ListingFeaturedTypes::getFeaturedText($listingFeaturedType));
  619. $rules = [
  620. 'featuredFees' => $fees,
  621. 'featuredDuration' => $duration,
  622. 'listingFeaturedType' => $listingFeaturedType,
  623. 'listingFeature' => $listingFeature,
  624. 'featuredText' => $featuredText,
  625. ];
  626. if ($this->listingManager->requiresFeaturingReview($listingFeaturedType)) {
  627. try {
  628. $this->listingManager->makeItFeatured($listing, $rules);
  629. if (ListingStatus::LIVE == $listingStatus) {
  630. $this->addFlash(
  631. 'success',
  632. $this->translator->trans('add_listing_page.success_messages.pending_featuring', [':featured_type:' => $featuredText])
  633. );
  634. } else {
  635. $this->addFlash(
  636. 'success',
  637. $this->translator->trans('add_listing_page.success_messages.padding_review')
  638. );
  639. if ($this->featureToggleManager->isEnabled('web.delayed.review.eid.info.message')) {
  640. $this->addFlash(
  641. 'info',
  642. $this->translator->trans('add_listing_page.info_messages.eid_delayed_review')
  643. );
  644. }
  645. }
  646. } catch (\Exception $exception) {
  647. $this->addFlash('danger', $this->translator->trans($exception->getMessage()));
  648. }
  649. if (ListingStatus::LIVE == $listingStatus) {
  650. return $this->redirectToRoute('aqarmap_listing_default_mylistings');
  651. }
  652. return $this->redirectToRoute('listing_finish', [
  653. 'id' => $listing->getId(),
  654. ]);
  655. }
  656. try {
  657. $this->listingManager->makeItFeatured($listing, $rules);
  658. if (ListingStatus::LIVE == $listing->getStatus()) {
  659. $this->addFlash(
  660. 'success',
  661. $this->translator->trans(
  662. 'add_listing_page.success_messages.featured',
  663. [':listing_title:' => $listing->getTitle(), ':featured_type:' => $featuredText]
  664. )
  665. );
  666. return $user->hasValidAccessToLiveApp() ?
  667. $this->redirect(sprintf('%s/%s', $this->getParameter('user_dashboard_url'), 'listings')) :
  668. $this->redirectToRoute('aqarmap_listing_default_mylistings');
  669. }
  670. $this->addFlash(
  671. 'success',
  672. $this->translator->trans('add_listing_page.success_messages.padding_review')
  673. );
  674. if ($this->featureToggleManager->isEnabled('web.delayed.review.eid.info.message')) {
  675. $this->addFlash(
  676. 'info',
  677. $this->translator->trans('add_listing_page.info_messages.eid_delayed_review')
  678. );
  679. }
  680. return $this->redirectToRoute('listing_finish', [
  681. 'id' => $listing->getId(),
  682. ]);
  683. } catch (\Exception $e) {
  684. $buyCreditLink = $this->generateUrl('page_view', ['slug' => 'buy-credit'], true);
  685. if ($this->setting->getSetting('features', 'payments')) {
  686. $buyCreditLink = $this->generateUrl('aqarmap_buy_credit');
  687. }
  688. $this->addFlash('danger', $this->translator->trans(
  689. $e->getMessage(),
  690. ['%link%' => $buyCreditLink],
  691. 'exceptions'
  692. ));
  693. }
  694. return $this->redirectToRoute('listing_confirm_featured_credit', [
  695. 'id' => $listing->getId(),
  696. ], Response::HTTP_FOUND);
  697. }
  698. /**
  699. * @return array|RedirectResponse
  700. */
  701. #[Route(path: '/listing/{id}/payment_confirmation', name: 'listing_confirm_publish_credit', requirements: ['id' => '\d+'], methods: ['GET'])]
  702. #[IsGranted(attribute: new Expression('is_granted("ROLE_USER") and is_granted("ROLE_OWNER", subject["listing"])'), subject: ['listing'])]
  703. public function confirmPublishing(Listing $listing, UserPackagesService $userPackagesService): Response
  704. {
  705. $listingRule = $this->listingRuleMatcher->match($listing);
  706. /** @var EntityManager $em */
  707. $em = $this->managerRegistry->getManager();
  708. // User Balance
  709. $userDetails = $listing->getUser();
  710. $available_balance = $this->userPackagesRepository->getTotalCreditsByUser($userDetails);
  711. $userId = $userDetails->getId();
  712. $em->getRepository(User::class)->findOneBy(['id' => $userId]);
  713. $listingManger = $this->listingManager;
  714. if (!$userPackagesService->hasAccessToList($listing->getUser(), $listing)) {
  715. $this->addFlash('danger', $this->translator->trans('credit.can_not_list_in_location'));
  716. return $this->redirectToRoute('listing_upload', [
  717. 'id' => $listing->getId(),
  718. ], Response::HTTP_FOUND);
  719. }
  720. $userServiceRepository = $em->getRepository(UserServices::class);
  721. if (
  722. $this->userServicesManager->hasActiveService(UserServicesType::UNLIMITED_LISTINGS, $userId)
  723. && ListingCategories::UNLIMITED != $listing->getCategory()
  724. && ListingStatus::DRAFT != $listing->getStatus()
  725. ) {
  726. if (
  727. $userServiceRepository
  728. ->passUnlimitedListing($listing, $this->creditManager, $listingManger) || $listing->getPublicationCredit()
  729. ) {
  730. if ($this->listingFeatureService->isFeaturedOffersAvailable($listing)) {
  731. return $this->redirectToRoute('listing_confirm_featured_credit', [
  732. 'id' => $listing->getId(),
  733. ]);
  734. }
  735. return $this->redirectToRoute('listing_finish', [
  736. 'id' => $listing->getId(),
  737. ]);
  738. }
  739. }
  740. $form = $this->ConfirmPaymentForm($listing);
  741. return $this->render(
  742. '@AqarmapListing/Listing/confirmPublishing.html.twig',
  743. [
  744. 'listing' => $listing,
  745. 'fees' => $listingRule['publication_fees'],
  746. 'duration' => $listingRule['duration'],
  747. 'available_balance' => $available_balance,
  748. 'form' => $form->createView(),
  749. ]
  750. );
  751. }
  752. /**
  753. * @return array
  754. */
  755. #[Route(path: '/listing/{id}/list_rejections', name: 'listing_list_rejections', requirements: ['id' => '\d+'], methods: ['GET'])]
  756. #[IsGranted(attribute: 'ROLE_OWNER', subject: 'listing')]
  757. public function listRejections(Listing $listing, Request $request): Response
  758. {
  759. if ($request->get('notification')) {
  760. try {
  761. $this->databaseNotification->markOneAsRead($request->get('notification'));
  762. } catch (\Exception $exception) {
  763. $this->logger->log(LogLevel::ERROR, $exception->getMessage());
  764. }
  765. }
  766. return $this->render(
  767. '@AqarmapListing/Listing/confirmPublishing.html.twig',
  768. [
  769. 'listing' => $listing,
  770. ]
  771. );
  772. }
  773. /**
  774. * @return array|RedirectResponse
  775. */
  776. #[Route(path: '/listing/{id}/payment_confirmation', name: 'listing_confirm_publish_credit_post', requirements: ['id' => '\d+'], methods: ['POST'])]
  777. #[IsGranted(attribute: new Expression('is_granted("ROLE_USER") and is_granted("ROLE_OWNER", subject["listing"])'), subject: ['listing'])]
  778. public function prepareListingPayment(Listing $listing, Request $request, UserPackagesService $userPackagesService): RedirectResponse
  779. {
  780. $listingRule = $this->listingRuleMatcher->match($listing);
  781. /** @var EntityManager $em */
  782. $em = $this->managerRegistry->getManager();
  783. // User Balance
  784. $available_balance = $this->userPackagesRepository->getTotalCreditsByUser($listing->getUser());
  785. $userId = $listing->getUser()->getId();
  786. $listingManger = $this->listingManager;
  787. if (!$userPackagesService->hasAccessToList($listing->getUser(), $listing)) {
  788. $this->addFlash('danger', $this->translator->trans('credit.can_not_list_in_location'));
  789. return $this->redirectToRoute('listing_upload', [
  790. 'id' => $listing->getId(),
  791. ], Response::HTTP_FOUND);
  792. }
  793. $userServiceRepository = $em->getRepository(UserServices::class);
  794. if ($this->userServicesManager->hasActiveService(UserServicesType::UNLIMITED_LISTINGS, $userId)) {
  795. if ($userServiceRepository->passUnlimitedListing($listing, $this->creditManager, $listingManger)) {
  796. if (!$this->listingFeatureService->isFeaturedOffersAvailable($listing)) {
  797. return $this->redirectToRoute('listing_finish', [
  798. 'id' => $listing->getId(),
  799. ]);
  800. }
  801. return $this->redirectToRoute('listing_confirm_featured_credit', [
  802. 'id' => $listing->getId(),
  803. ]);
  804. }
  805. }
  806. $form = $this->ConfirmPaymentForm($listing);
  807. $form->handleRequest($request);
  808. if ($form->isSubmitted() && $form->isValid()) {
  809. // Payment Confirmed?
  810. // If user cancel payment redirect him to his listing,
  811. if (!$form->get('confirm')->isClicked()) {
  812. return $this->redirectToRoute('listing_view', [
  813. 'id' => $listing->getId(),
  814. ], Response::HTTP_FOUND);
  815. }
  816. // If user credit expired, complain.
  817. $enforceCredit = $this->setting->getSetting('features', 'enforce_credits_expiration');
  818. $owner = $listing->getUser();
  819. if ($enforceCredit && 0 === $owner->getAbsoluteCreditExpiryDays()) {
  820. $this->addFlash('danger', $this->translator->trans('credit.credit_can_not_use', [
  821. 'link' => $this->generateUrl('aqarmap_buy_credit', ['listing_id' => $listing->getId()]),
  822. ]));
  823. return $this->redirectToRoute('listing_confirm_publish_credit', [
  824. 'id' => $listing->getId(),
  825. ], Response::HTTP_FOUND);
  826. }
  827. // User already paid
  828. if ($listing->getPublicationCredit()) {
  829. $this->addFlash('info', $this->translator->trans('credit.already_paid'));
  830. } else {
  831. // User don't have enough credits
  832. if ($listingRule['publication_fees'] > $available_balance) {
  833. $this->addFlash('danger', $this->translator->trans('credit.not_enough_credits'));
  834. } else {
  835. // Subtract publication fees
  836. $credits = $this->creditManager->deduction(
  837. $listing->getUser(),
  838. $listingRule['publication_fees'],
  839. 'Listing Fees'
  840. );
  841. foreach ($credits as $credit) {
  842. if ($credit instanceof Credit) {
  843. $credit->setStatus(CreditStatus::SUCCESS);
  844. $this->listingManager->addFeature($listing, ListingFeatures::PAID, null, $credit);
  845. if (!$this->listingFeatureService->isFeaturedOffersAvailable($listing)) {
  846. return $this->redirectToRoute('listing_finish', [
  847. 'id' => $listing->getId(),
  848. ]);
  849. }
  850. }
  851. }
  852. return $this->redirectToRoute('listing_confirm_featured_credit', [
  853. 'id' => $listing->getId(),
  854. 'ref' => 'add_listing',
  855. ]);
  856. }
  857. }
  858. }
  859. $this->addFlash('danger', 'Unexpected error occurred.');
  860. return $this->redirectToRoute('listing_confirm_publish_credit', [
  861. 'id' => $listing->getId(),
  862. ]);
  863. }
  864. private function ConfirmPaymentForm(Listing $listing)
  865. {
  866. return $this->createForm(ConfirmFormType::class, null, [
  867. 'method' => 'POST',
  868. 'action' => $this->generateUrl('listing_confirm_publish_credit_post', ['id' => $listing->getId()]),
  869. ]);
  870. }
  871. /**
  872. * @return \Symfony\Component\Form\Form
  873. */
  874. private function confirmFeaturedPaymentForm(Listing $listing)
  875. {
  876. return $this->createForm(ConfirmFeaturedFormType::class, null, [
  877. 'method' => 'POST',
  878. 'action' => $this
  879. ->generateUrl(
  880. 'listing_confirm_publish_featured_credit_post',
  881. ['id' => $listing->getId()]
  882. ),
  883. ]);
  884. }
  885. /**
  886. * Delete Listing entity.
  887. */
  888. #[Route(path: '/listing/{id}/delete', name: 'listing_delete', requirements: ['id' => '\d+'], options: ['expose' => true])]
  889. #[IsGranted(attribute: new Expression('is_granted("ROLE_USER") and is_granted("ROLE_OWNER", subject["listing"])'), subject: ['listing'])]
  890. public function delete(Listing $listing, Request $request): RedirectResponse
  891. {
  892. if (\in_array('ROLE_PREVENT_DELETE_LISTING', $this->getUser()->getRoles())) {
  893. throw new AccessDeniedHttpException("Forbidden, user don't have this permission.");
  894. }
  895. if (ListingStatus::EXPIRED == $listing->getStatus()) {
  896. throw $this->createNotFoundException('Listing is expired.');
  897. }
  898. $this->listingManager->remove($listing, ListingStatus::USER_DELETED);
  899. $this->addFlash(
  900. 'success',
  901. $this->translator->trans('add_listing_page.success_messages.deleted')
  902. );
  903. return $this->redirect($request->headers->get('referer') ?: $this->generateUrl('homepage'));
  904. }
  905. /**
  906. * unDelete Listing entity.
  907. */
  908. #[Route(path: '/listing/{id}/undelete', name: 'listing_undelete', requirements: ['id' => '\d+'])]
  909. #[IsGranted(attribute: new Expression('is_granted("ROLE_USER") and is_granted("ROLE_OWNER", subject["listing"])'), subject: ['listing'])]
  910. public function undelete(Listing $listing): Response
  911. {
  912. if (ListingStatus::USER_DELETED != $listing->getStatus()) {
  913. throw $this->createNotFoundException('Unable to find this listing.');
  914. }
  915. $this->managerRegistry->getManager();
  916. $this->listingManager->changeStatus($listing, ListingStatus::PENDING);
  917. $listingEvent = new ListingEvent($listing);
  918. $this->dispatcher->dispatch($listingEvent, 'aqarmap.listing.submitted');
  919. $response = $listingEvent->getResponse();
  920. if (null === $response) {
  921. if (ListingStatus::PENDING === $listing->getStatus()) {
  922. $this->addFlash(
  923. 'success',
  924. $this->translator->trans('add_listing_page.success_messages.padding_review')
  925. );
  926. if ($this->featureToggleManager->isEnabled('web.delayed.review.eid.info.message')) {
  927. $this->addFlash(
  928. 'info',
  929. $this->translator->trans('add_listing_page.info_messages.eid_delayed_review')
  930. );
  931. }
  932. }
  933. $response = new RedirectResponse(
  934. $this->generateUrl(
  935. 'listing_slug',
  936. ['id' => $listing->getId(), 'slug' => $listing->getSlug()]
  937. )
  938. );
  939. }
  940. return $response;
  941. }
  942. /**
  943. * Creates "Request a Call" button.
  944. *
  945. * @return \Symfony\Component\Form\Form
  946. */
  947. public function callRequestForm(Listing $listing)
  948. {
  949. return $this->createForm(CallRequestFormType::class, null, [
  950. 'method' => 'POST',
  951. 'action' => $this->generateUrl('aqarmap_listing_call_request', ['id' => $listing->getId()]),
  952. ]);
  953. }
  954. /**
  955. * Creates contact seller form.
  956. *
  957. * @param Listing $listing The listing entity
  958. *
  959. * @return \Symfony\Component\Form\Form The form
  960. */
  961. public function contactSellerForm(Listing $listing)
  962. {
  963. return $this->createForm(ContactSellerFormType::class, null, [
  964. 'method' => 'POST',
  965. 'action' => $this->generateUrl('aqarmap_listing_contact_seller', ['id' => $listing->getId()]),
  966. ]);
  967. }
  968. /**
  969. * Creates contact seller form.
  970. *
  971. * @param Listing $listing The listing entity
  972. *
  973. * @return \Symfony\Component\Form\Form The form
  974. */
  975. public function contactSellerWideForm(Listing $listing)
  976. {
  977. return $this->createForm(ContactSellerWideFormType::class, null, [
  978. 'method' => 'POST',
  979. 'action' => $this->generateUrl('aqarmap_listing_contact_seller', ['id' => $listing->getId()]),
  980. ]);
  981. }
  982. /**
  983. * Creates quick registration form.
  984. *
  985. * @return \Symfony\Component\Form\Form The form
  986. */
  987. public function quickRegistrationForm()
  988. {
  989. return $this->createForm(QuickRegistrationFormType::class, null, [
  990. 'method' => 'POST',
  991. 'action' => $this->generateUrl('aqarmap_user_quick_registration'),
  992. ]);
  993. }
  994. /**
  995. * Contact Seller Action.
  996. *
  997. * @return array|\Symfony\Component\Form\Form
  998. */
  999. #[Route(path: '/listing/{id}/contact_seller', name: 'aqarmap_listing_contact_seller', requirements: ['id' => '\d+'], options: ['expose' => true], defaults: ['_format' => 'json'], methods: ['POST'])]
  1000. #[Rest\View]
  1001. public function contactSeller(Listing $listing, Request $request)
  1002. {
  1003. $form = $this->contactSellerForm($listing);
  1004. if ($request->request->has('contact_seller_wide')) {
  1005. $form = $this->contactSellerWideForm($listing);
  1006. }
  1007. $lead = $request->get('lead');
  1008. $originalPhoneNumber = $lead['phone'];
  1009. $phoneNumber = $lead ? $this->phoneManager->trimZero($lead['phone'], $request->get('countryCode')) : null;
  1010. $message = json_decode((string) $request->get('message'), true);
  1011. $hasEmail = !$request->get('isAutoGeneratedEmail', 0);
  1012. $user = $this->getUser();
  1013. $form->handleRequest($request);
  1014. if ($lead) {
  1015. $form = $this->createForm(QuickLeadType::class, null, [
  1016. 'method' => 'POST',
  1017. 'csrf_protection' => false,
  1018. ]);
  1019. /** @var User $user */
  1020. $user = $this->FOSUserManager->findUserByEmail($lead['email']);
  1021. if (!$user && !$hasEmail) {
  1022. $user = $this->userManager->findLatestByPhone($phoneNumber, $request->get('countryCode'));
  1023. }
  1024. if (!$user && $hasEmail) {
  1025. $user = $this->userManager->findAndReplaceUserAndEmail($phoneNumber, $lead['email'], $request->get('countryCode'));
  1026. }
  1027. $phone = $this->phoneManager->findOrSavePhoneNumber($phoneNumber, $request->get('countryCode'), null, $originalPhoneNumber);
  1028. if (!$user) {
  1029. $user = $this->FOSUserManager->createUser();
  1030. $user
  1031. ->setFullName($lead['name'])
  1032. ->setPhoneNumber($lead['phone'])
  1033. ->setTempOriginalPhoneNumber($originalPhoneNumber)
  1034. ->setEmail($lead['email'])
  1035. ->setHasEmail($hasEmail)
  1036. ->setIsQucikRegistered(true)
  1037. ;
  1038. $this->userManager->quickRegister($user, $form, $request, true, false, false);
  1039. $registerMessage = $this->translator->trans('popup_form.success');
  1040. }
  1041. $this->phoneManager->addNewUserPhone($phoneNumber, $request->get('countryCode'), $user, true, false, null, $originalPhoneNumber);
  1042. }
  1043. if ($request->isMethod('POST')) {
  1044. // Get current logged in user
  1045. $user ??= $this->getUser();
  1046. $composer = $this->messageComposer;
  1047. $composer
  1048. ->setSender($user)
  1049. ->compose($message['content'], $listing, null, $message['type'] ?? LeadTypes::SEND_MESSAGE)
  1050. ;
  1051. $this->addFlash(
  1052. 'success',
  1053. $this->translator->trans('add_listing_page.success_messages.message_sent')
  1054. );
  1055. return [
  1056. 'status' => 'ok',
  1057. 'message' => $this->translator->trans('listing.success_message_seller'),
  1058. ];
  1059. }
  1060. return $form;
  1061. }
  1062. /**
  1063. * Contact Seller Action.
  1064. */
  1065. #[Route(path: '/listing/{id}/call_request', name: 'aqarmap_listing_call_request', requirements: ['id' => '\d+', '_format' => 'json'], options: ['expose' => true], defaults: ['_format' => 'json'], methods: ['POST'])]
  1066. #[Rest\View]
  1067. public function callRequest(Request $request, Listing $listing)
  1068. {
  1069. $lead = $request->get('lead');
  1070. $countryCode = $request->get('countryCode', $lead['countryCode'] ?? '+20');
  1071. $originalPhoneNumber = $lead['phone'];
  1072. $phoneNumber = $lead ? $this->phoneManager->trimZero($lead['phone'], $countryCode) : null;
  1073. $user = $this->getUser();
  1074. $registerMessage = '';
  1075. $callRequest = new CallRequest();
  1076. if ($lead) {
  1077. $form = $this->createForm(QuickLeadType::class, null, [
  1078. 'method' => 'POST',
  1079. 'csrf_protection' => false,
  1080. ]);
  1081. $form->handleRequest($request);
  1082. $hasEmail = !$request->get('isAutoGeneratedEmail', 0);
  1083. /** @var User $user */
  1084. $user = $this->FOSUserManager->findUserByEmail($lead['email']);
  1085. if (!$user && !$hasEmail) {
  1086. $user = $this->userManager->findLatestByPhone($phoneNumber, $countryCode);
  1087. }
  1088. if (!$user && $hasEmail) {
  1089. $user = $this->userManager->findAndReplaceUserAndEmail($phoneNumber, $lead['email'], $request->get('countryCode'));
  1090. }
  1091. $phone = $this->phoneManager->findOrSavePhoneNumber($phoneNumber, $countryCode, null, $originalPhoneNumber);
  1092. if (!$user) {
  1093. $user = $this->FOSUserManager->createUser();
  1094. $user
  1095. ->setFullName($lead['name'])
  1096. ->setPhoneNumber($lead['phone'])
  1097. ->setTempCountryCode($countryCode)
  1098. ->setTempOriginalPhoneNumber($originalPhoneNumber)
  1099. ->setEmail($lead['email'])
  1100. ->setHasEmail($hasEmail)
  1101. ->setIsQucikRegistered(true)
  1102. ;
  1103. $this->userManager->quickRegister($user, $form, $request, true, false, false);
  1104. $registerMessage = $this->translator->trans('popup_form.success');
  1105. }
  1106. $callRequest->setPhone($phone);
  1107. $callRequest->setLeadEmail($lead['email']);
  1108. $callRequest->setLeadFullName($lead['name']);
  1109. $this->phoneManager->addNewUserPhone($phoneNumber, $countryCode, $user, true, false, null, $originalPhoneNumber);
  1110. }
  1111. // Create Call Request
  1112. $callRequest->setUser($user);
  1113. $callRequest->setListing($listing);
  1114. $form = $this->callRequestForm($listing);
  1115. $form->handleRequest($request);
  1116. $isPostRequestAndValidForm = $request->isMethod('POST') && ($form->isSubmitted() && $form->isValid());
  1117. $isNotCheckForm = (false == $request->get('check_form'));
  1118. if ($isPostRequestAndValidForm || $isNotCheckForm) {
  1119. try {
  1120. $this->callRequestManager->submitCallRequest($callRequest, null, $request->query->get('sourceRoute', ''));
  1121. return [
  1122. 'status' => 'ok',
  1123. 'message' => $this->translator->trans('listing.success_call_request'),
  1124. 'registerMessage' => $registerMessage,
  1125. ];
  1126. } catch (InvalidLeadValidationHttpException $exception) {
  1127. return [
  1128. 'status' => $exception->getStatusCode(),
  1129. 'message' => $exception->getMessage(),
  1130. ];
  1131. }
  1132. }
  1133. return $form;
  1134. }
  1135. /**
  1136. * Show how to add a listing for new users.
  1137. */
  1138. #[Route(path: '/add_listing', name: 'aqarmap_add_listing')]
  1139. public function addListingRoles(): Response
  1140. {
  1141. return $this->render(
  1142. '@AqarmapListing/Listing/addListingRoles.html.twig',
  1143. []
  1144. );
  1145. }
  1146. /**
  1147. * @return RedirectResponse|Response
  1148. */
  1149. #[Route(path: '/listing/{id}/relist', name: 'aqarmap_listing_relist', requirements: ['id' => '\d+'], options: ['expose' => true])]
  1150. #[IsGranted(attribute: new Expression('is_granted("ROLE_USER") and is_granted("ROLE_OWNER", subject["listing"])'), subject: ['listing'])]
  1151. public function relist(Listing $listing)
  1152. {
  1153. if (!\in_array($listing->getStatus(), [ListingStatus::EXPIRED, ListingStatus::USER_DELETED])) {
  1154. throw new LogicHttpException('Whoops! Looks like the listing you are trying to relist in not expired!');
  1155. }
  1156. $em = $this->managerRegistry->getManager();
  1157. $listingRepo = $em->getRepository(Listing::class);
  1158. $relistChild = $listingRepo->getRelistChild($listing);
  1159. if ($relistChild) {
  1160. $this->addFlash(
  1161. 'success',
  1162. $this->translator->trans('add_listing_page.success_messages.relist')
  1163. );
  1164. return new RedirectResponse(
  1165. $this
  1166. ->generateUrl(
  1167. 'listing_slug',
  1168. [
  1169. 'id' => $relistChild->getId(),
  1170. 'slug' => $relistChild->getSlug(),
  1171. ]
  1172. )
  1173. );
  1174. }
  1175. $listing = $this->listingManager->relist($listing);
  1176. // Check if the listing requires more photos or requires repayment, and redirection.
  1177. $listingEvent = new ListingEvent($listing);
  1178. $this->dispatcher->dispatch($listingEvent, 'aqarmap.listing.resubmitted');
  1179. $response = $listingEvent->getResponse();
  1180. if (null === $response) {
  1181. if (\in_array($listing->getStatus(), [ListingStatus::PENDING, ListingStatus::LIVE])) {
  1182. $this->addFlash(
  1183. 'success',
  1184. $this->translator->trans('add_listing_page.success_messages.relist')
  1185. );
  1186. }
  1187. $response = new RedirectResponse(
  1188. $this
  1189. ->generateUrl(
  1190. 'listing_slug',
  1191. [
  1192. 'id' => $listing->getId(),
  1193. 'slug' => $listing->getSlug(),
  1194. ]
  1195. )
  1196. );
  1197. }
  1198. return $response;
  1199. }
  1200. #[Route(path: '/listing/{id}/pump_up', name: 'confirm_listing_pump_up', requirements: ['id' => '\d+'], methods: ['POST'])]
  1201. #[IsGranted(attribute: 'ROLE_OWNER', subject: 'listing')]
  1202. public function confirmPumpUpListing(Listing $listing, Request $request): RedirectResponse
  1203. {
  1204. $pumpUpForm = $this->confirmFeaturedPaymentForm($listing);
  1205. $pumpUpForm->handleRequest($request);
  1206. if ($request->isMethod('POST') && $pumpUpForm->isValid()) {
  1207. if (!$pumpUpForm->get('confirm')->isClicked()) {
  1208. return $this->redirectToRoute('aqarmap_listing_default_mylistings', [], Response::HTTP_FOUND);
  1209. }
  1210. try {
  1211. $this->listingManager->pumpUp($listing);
  1212. $this->addFlash(
  1213. 'success',
  1214. $this->translator->trans('credit.pump_up_success', [':listing_title:' => $listing->getTitle()])
  1215. );
  1216. } catch (\Exception $e) {
  1217. $this->addFlash('danger', $e->getMessage());
  1218. }
  1219. }
  1220. return $this->redirectToRoute('aqarmap_listing_default_mylistings');
  1221. }
  1222. /**
  1223. * @return array|RedirectResponse
  1224. */
  1225. #[Route(path: '/listing/{id}/pump_up', name: 'listing_pump_up', requirements: ['id' => '\d+'], methods: ['GET'])]
  1226. #[IsGranted(attribute: 'ROLE_OWNER', subject: 'listing')]
  1227. public function pumpUpListing(Listing $listing)
  1228. {
  1229. $listingRules = $this->listingManager->getListingRules($listing);
  1230. $listingOwner = $listing->getUser();
  1231. $pumpUpFees = $listingRules['pump_up_fees'];
  1232. $pumpUpOccurrence = $listingRules['pump_up_occurrence'];
  1233. $pumpUpDuration = $listingRules['pump_up_duration'];
  1234. if (!$this->listingManager->isPumpUpAvailable($listingRules)) {
  1235. $this->addFlash(
  1236. 'danger',
  1237. $this->translator->trans('credit.pump_up_not_available')
  1238. );
  1239. return $this->redirectToRoute('aqarmap_listing_default_mylistings');
  1240. }
  1241. if (!$this->listingManager->isAffordable($listingOwner, $pumpUpFees)) {
  1242. $this->addFlash(
  1243. 'danger',
  1244. $this->translator->trans('credit.pump_up_can_not_use')
  1245. );
  1246. return $this->redirectToRoute('aqarmap_listing_default_mylistings');
  1247. }
  1248. /** @var EntityManager $em */
  1249. $em = $this->managerRegistry->getManager();
  1250. $em->getRepository(Credit::class);
  1251. $pumpUpForm = $this->createForm(ConfirmFeaturedFormType::class, null, [
  1252. 'method' => 'POST',
  1253. 'action' => $this
  1254. ->generateUrl(
  1255. 'listing_pump_up',
  1256. ['id' => $listing->getId()]
  1257. ),
  1258. 'confirm_message' => $this->translator->trans('listing.pump_up.confirm'),
  1259. 'cancel_message' => $this->translator->trans('listing.pump_up.cancel'),
  1260. ]);
  1261. return $this->render(
  1262. '@AqarmapListing/Listing/pumpUpListing.html.twig',
  1263. [
  1264. 'fees' => $pumpUpFees,
  1265. 'duration' => $pumpUpDuration,
  1266. 'occurrence' => $pumpUpOccurrence,
  1267. 'form' => $pumpUpForm,
  1268. ]
  1269. );
  1270. }
  1271. /**
  1272. * Delete Listing Photos.
  1273. *
  1274. * @throws \Doctrine\ORM\OptimisticLockException
  1275. */
  1276. #[Route(path: '/photos/delete', name: 'listing_bulk_delete_photo', requirements: ['id' => '\d+'])]
  1277. public function bulkDeletePhoto(Request $request, ListingPhotoRepository $listingPhotoRepository, ListingManager $listingManager): RedirectResponse
  1278. {
  1279. $photosIds = $request->get('photos');
  1280. try {
  1281. if ($photosIds) {
  1282. $photos = $listingPhotoRepository->getPhotos($photosIds);
  1283. $listingManager->bulkDeletePhotos($photos);
  1284. } else {
  1285. $this->addFlash('danger', 'No photos selected for deletion');
  1286. }
  1287. } catch (\Exception) {
  1288. $this->addFlash('danger', 'Something went wrong');
  1289. }
  1290. return $this->redirect($request->headers->get('referer') ?: $this->generateUrl('homepage'));
  1291. }
  1292. /**
  1293. * @throws \Exception
  1294. */
  1295. #[Route(path: '/brochure/{file}/download', name: 'download_brochure')]
  1296. public function downloadBrochure(File $file, DownloadHandler $downloadHandler): Response
  1297. {
  1298. $response = $downloadHandler->downloadObject($file, 'file');
  1299. // force specific content type instead of using default "application/octet-stream".
  1300. $response->headers->set('content-type', 'application/pdf');
  1301. return $response;
  1302. }
  1303. /**
  1304. * Update WhatsApp Phones.
  1305. */
  1306. private function updateWhatsAppPhones(int $userId, array $phones = [], array $whatsApp = []): void
  1307. {
  1308. foreach ($phones as $index => $phone) {
  1309. $whatsApp[$index] = isset($whatsApp[$index]) && 'true' === $whatsApp[$index];
  1310. $this->phoneManager->updateWhatsApp($userId, (int) $phone, $whatsApp[$index], true);
  1311. }
  1312. }
  1313. }