src/Aqarmap/Bundle/ListingBundle/Controller/Api/ListingController.php line 92

  1. <?php
  2. namespace Aqarmap\Bundle\ListingBundle\Controller\Api;
  3. use App\Exception\BadRequestHttpException;
  4. use App\Exception\LogicHttpException;
  5. use Aqarmap\Bundle\CreditBundle\Constant\CreditStatus;
  6. use Aqarmap\Bundle\CreditBundle\Contract\CreditManagerInterface;
  7. use Aqarmap\Bundle\CreditBundle\Entity\Credit;
  8. use Aqarmap\Bundle\FeatureToggleBundle\Service\FeatureToggleManager;
  9. use Aqarmap\Bundle\ListingBundle\Constant\CountryCodes;
  10. use Aqarmap\Bundle\ListingBundle\Constant\LeadTypes;
  11. use Aqarmap\Bundle\ListingBundle\Constant\ListingFeaturedTypes;
  12. use Aqarmap\Bundle\ListingBundle\Constant\ListingFeatures;
  13. use Aqarmap\Bundle\ListingBundle\Constant\ListingStatus;
  14. use Aqarmap\Bundle\ListingBundle\Constant\ListingSyncedFields;
  15. use Aqarmap\Bundle\ListingBundle\Constant\PhotoTypes;
  16. use Aqarmap\Bundle\ListingBundle\Contracts\PhoneManagerInterface;
  17. use Aqarmap\Bundle\ListingBundle\Entity\CallRequest;
  18. use Aqarmap\Bundle\ListingBundle\Entity\Listing;
  19. use Aqarmap\Bundle\ListingBundle\Entity\ListingNote;
  20. use Aqarmap\Bundle\ListingBundle\Entity\ListingPhone;
  21. use Aqarmap\Bundle\ListingBundle\Entity\ListingPhoto;
  22. use Aqarmap\Bundle\ListingBundle\Entity\Photo;
  23. use Aqarmap\Bundle\ListingBundle\Entity\PropertyType;
  24. use Aqarmap\Bundle\ListingBundle\Entity\Section;
  25. use Aqarmap\Bundle\ListingBundle\Event\LeadEvent;
  26. use Aqarmap\Bundle\ListingBundle\Event\ListingEvent;
  27. use Aqarmap\Bundle\ListingBundle\Event\ListingUpdatedEvent;
  28. use Aqarmap\Bundle\ListingBundle\Form\ContactSellerFormType;
  29. use Aqarmap\Bundle\ListingBundle\Form\ListingApiType;
  30. use Aqarmap\Bundle\ListingBundle\Form\ListingNoteType;
  31. use Aqarmap\Bundle\ListingBundle\Form\PhotoType;
  32. use Aqarmap\Bundle\ListingBundle\Form\QuickContactSellerType;
  33. use Aqarmap\Bundle\ListingBundle\Form\QuickCreateLeadFormType;
  34. use Aqarmap\Bundle\ListingBundle\Form\QuickLeadType;
  35. use Aqarmap\Bundle\ListingBundle\Model\LeadModel;
  36. use Aqarmap\Bundle\ListingBundle\Repository\ListingNoteRepository;
  37. use Aqarmap\Bundle\ListingBundle\Repository\ListingRepository;
  38. use Aqarmap\Bundle\ListingBundle\Service\CallRequestManager;
  39. use Aqarmap\Bundle\ListingBundle\Service\FavouriteService;
  40. use Aqarmap\Bundle\ListingBundle\Service\InteractionService;
  41. use Aqarmap\Bundle\ListingBundle\Service\LeadService;
  42. use Aqarmap\Bundle\ListingBundle\Service\ListingManager;
  43. use Aqarmap\Bundle\ListingBundle\Service\ListingNoteService;
  44. use Aqarmap\Bundle\ListingBundle\Service\ListingRateService;
  45. use Aqarmap\Bundle\ListingBundle\Service\ListingRuleMatcher;
  46. use Aqarmap\Bundle\ListingBundle\Service\LocationManager;
  47. use Aqarmap\Bundle\ListingBundle\Service\Mortgage\MortgageService;
  48. use Aqarmap\Bundle\ListingBundle\Service\NewsFeed\ListingNewsFeed;
  49. use Aqarmap\Bundle\ListingBundle\Twig\ListingExtension;
  50. use Aqarmap\Bundle\MainBundle\Controller\Api\BaseController;
  51. use Aqarmap\Bundle\MainBundle\Service\Setting;
  52. use Aqarmap\Bundle\MessageBundle\Service\Composer;
  53. use Aqarmap\Bundle\TopSellerBundle\Model\TopSeller;
  54. use Aqarmap\Bundle\TopSellerBundle\Service\TopSellerRetrievalService;
  55. use Aqarmap\Bundle\UserBundle\Entity\User;
  56. use Aqarmap\Bundle\UserBundle\Services\UserManager;
  57. use Doctrine\Common\Collections\Collection;
  58. use Doctrine\ORM\EntityManager;
  59. use Doctrine\ORM\EntityManagerInterface;
  60. use Doctrine\ORM\OptimisticLockException;
  61. use Doctrine\ORM\ORMException;
  62. use FOS\RestBundle\Controller\Annotations as Rest;
  63. use FOS\RestBundle\View\View;
  64. use FOS\UserBundle\Model\UserManagerInterface;
  65. use Gedmo\Translatable\TranslatableListener;
  66. use Knp\Component\Pager\PaginatorInterface;
  67. use OpenApi\Attributes as OA;
  68. use Sensio\Bundle\FrameworkExtraBundle\Configuration\ParamConverter;
  69. use Symfony\Bridge\Doctrine\Attribute\MapEntity;
  70. use Symfony\Component\EventDispatcher\EventDispatcherInterface;
  71. use Symfony\Component\ExpressionLanguage\Expression;
  72. use Symfony\Component\Form\Form;
  73. use Symfony\Component\HttpFoundation\File\UploadedFile;
  74. use Symfony\Component\HttpFoundation\Request;
  75. use Symfony\Component\HttpFoundation\Response;
  76. use Symfony\Component\HttpKernel\Attribute\Cache;
  77. use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
  78. use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
  79. use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface;
  80. use Symfony\Component\Security\Http\Attribute\IsGranted;
  81. use Symfony\Contracts\Translation\TranslatorInterface;
  82. /**
  83. * Class ListingController.
  84. */
  85. class ListingController extends BaseController
  86. {
  87. /**
  88. * @var TokenStorageInterface
  89. */
  90. protected $tokenStorage;
  91. /** @var FOSUserManager */
  92. private $fosUserManager;
  93. public function __construct(
  94. private readonly Composer $messageComposer,
  95. private readonly FavouriteService $favouriteService,
  96. private readonly PaginatorInterface $paginator,
  97. private readonly InteractionService $interactionService,
  98. private readonly ListingManager $listingManager,
  99. private readonly EventDispatcherInterface $dispatcher,
  100. UserManagerInterface $fosUserManager,
  101. private readonly TranslatorInterface $translator,
  102. private readonly TranslatableListener $translatableListener,
  103. private readonly UserManager $userManager,
  104. private readonly ListingManager $listingService,
  105. private readonly ListingNoteService $listingNoteService,
  106. private readonly ListingRuleMatcher $listingRuleMatcher,
  107. private readonly LocationManager $locationManager,
  108. Setting $setting,
  109. private readonly PhoneManagerInterface $phoneManager,
  110. private readonly FeatureToggleManager $featureToggle,
  111. private readonly ListingRateService $listingRateService,
  112. TokenStorageInterface $tokenStorage,
  113. private readonly LeadService $leadManager,
  114. private readonly CallRequestManager $callRequestManager,
  115. private readonly MortgageService $mortgageService,
  116. private readonly CreditManagerInterface $creditManager,
  117. private readonly ListingExtension $listingExtension,
  118. private readonly ListingNewsFeed $listingNewsFeed,
  119. private readonly EntityManagerInterface $entityManager,
  120. private readonly \Doctrine\Persistence\ManagerRegistry $managerRegistry,
  121. ) {
  122. $this->fosUserManager = $fosUserManager;
  123. $this->tokenStorage = $tokenStorage;
  124. }
  125. /**
  126. * Get Listing.
  127. *
  128. * )
  129. */
  130. #[Rest\Get('/api/v2/listing/{id}', requirements: ['id' => '\d+'], options: ['i18n' => false], name: 'aqarmap_api_get_listing_v2')]
  131. #[Rest\View(serializerGroups: ['Default', 'Details', 'Compound'])]
  132. #[Cache(expires: '+2 hours', maxage: '+2 hours', smaxage: '+2 hours', public: false, vary: ['Accept-Language', 'X-Accept-Version', 'Accept'])]
  133. #[OA\Response(response: 404, description: 'Returned when the listing is not found')]
  134. public function getListing(Listing $listing): array
  135. {
  136. return ['listing' => $listing];
  137. }
  138. /**
  139. * Listing details.
  140. */
  141. #[Rest\Get('/api/v4/listing/{id}', requirements: ['id' => '\d+'], options: ['i18n' => false], name: 'aqarmap_api_get_listing_v4')]
  142. #[Rest\View(serializerGroups: ['listingDetails', 'listingDetailsWithLocationCompound'])]
  143. #[Cache(expires: '+6 hours', maxage: '+6 hours', smaxage: '+6 hours', public: true, vary: ['Accept-Language', 'X-Accept-Version', 'Accept'])]
  144. public function getListingDetails(int $id, ListingRepository $listingRepository, ListingManager $listingManager): array
  145. {
  146. $entityManagerFilters = $this->entityManager->getFilters();
  147. if ($entityManagerFilters->isEnabled('softdeleteable')) {
  148. $entityManagerFilters->disable('softdeleteable');
  149. }
  150. $listing = $listingRepository->find($id);
  151. $entityManagerFilters->enable('softdeleteable');
  152. return ['listing' => $listingManager->serializeListing($listing)];
  153. }
  154. /**
  155. * Get Listing Children (Project Units).
  156. *
  157. * )
  158. *
  159. * @return array
  160. */
  161. #[Rest\Get('/api/v2/listing/{id}/children', options: ['i18n' => false], name: 'aqarmap_api_get_listing_children_v2')]
  162. #[Rest\View(serializerGroups: ['Default', 'Details'])]
  163. #[OA\Response(response: 404, description: 'Returned when the listing is not found')]
  164. public function getListingChildren(Listing $listing)
  165. {
  166. return $this->respond($listing->getLiveChildren());
  167. }
  168. /**
  169. * @return Collection
  170. *
  171. * @throws ORMException
  172. * @throws OptimisticLockException
  173. */
  174. #[Rest\Post('/api/v2/listing/{listing}/phones', options: ['i18n' => false], name: 'aqarmap_api_get_listing_phones_v2')]
  175. #[Rest\View(serializerGroups: ['Default', 'Details'])]
  176. #[OA\Parameter(name: 'lead', in: 'query', description: '', required: false)]
  177. public function getListingPhone(Listing $listing, Request $request, ?User $user = null)
  178. {
  179. $version = (float) ltrim((string) $request->headers->get('X-Accept-Version'), 'v');
  180. $leadModel = new LeadModel();
  181. $currentUrl = $request->getUri();
  182. if (str_contains($currentUrl, 'v2')) {
  183. if (!$user = $this->getUser() && $version <= 2.13) {
  184. throw new AccessDeniedHttpException();
  185. }
  186. }
  187. $form = $this->createForm(QuickLeadType::class, null, [
  188. 'method' => 'POST',
  189. 'csrf_protection' => false,
  190. ]);
  191. $form->handleRequest($request);
  192. $lead = $form->getData();
  193. $phoneManager = $this->phoneManager;
  194. $originalPhoneNumber = $lead['phone'];
  195. $phoneNumber = $phoneManager->trimZero($lead['phone'], $lead['countryCode']);
  196. $aqarmapUserService = $this->userManager;
  197. $hasEmail = !$lead['isAutoGeneratedEmail'];
  198. /** @var UserManagerInterface $userManager */
  199. $userManager = $this->fosUserManager;
  200. /** @var User $user */
  201. $user = $userManager->findUserByEmail($lead['email']);
  202. $countryCode = $lead['countryCode'];
  203. if (!$user && !$hasEmail) {
  204. $user = $aqarmapUserService->findLatestByPhone($phoneNumber, $countryCode);
  205. }
  206. if (!$user && $hasEmail) {
  207. $user = $aqarmapUserService->findAndReplaceUserAndEmail($phoneNumber, $lead['email'], $countryCode);
  208. }
  209. if (!$user) {
  210. $user = $userManager->createUser();
  211. $user
  212. ->setFullName($lead['name'])
  213. ->setPhoneNumber($lead['phone'])
  214. ->setTempOriginalPhoneNumber($originalPhoneNumber)
  215. ->setTempCountryCode($lead['countryCode'])
  216. ->setEmail($lead['email'])
  217. ->setHasEmail($hasEmail)
  218. ->setLanguage('ar');
  219. $user = $aqarmapUserService->quickRegistration($form, $user, $request);
  220. }
  221. $phone = $phoneManager->addNewUserPhone($phoneNumber, $lead['countryCode'], $user, true, false, null, $originalPhoneNumber);
  222. $userManager->updateUser($user);
  223. $userManager->reloadUser($user);
  224. $leadModel->setPhone($phone->getPhone());
  225. $leadModel->setName($request->request->get('name', $lead['name']));
  226. $leadModel->setEmail($request->request->get('email', $lead['email']));
  227. $leadService = $this->leadManager;
  228. $leadModel->setLeadType(LeadTypes::SHOW_PHONE);
  229. $leadModel->setListing($listing);
  230. $leadModel->setSource('api');
  231. $leadModel->setUser($user);
  232. $lead = $leadService->addLead($leadModel);
  233. if ($lead) {
  234. $this->dispatcher->dispatch(
  235. new LeadEvent(
  236. $listing,
  237. $user,
  238. $lead
  239. ),
  240. 'aqarmap.listing.show_seller_number'
  241. );
  242. }
  243. return $listing->getPhones();
  244. }
  245. /**
  246. * @return bool|Form
  247. *
  248. * @throws AccessDeniedHttpException
  249. */
  250. #[Rest\Post('/api/v2/listing/{listing}/contact_seller', options: ['i18n' => false], name: 'aqarmap_api_listing_contact_seller_v2')]
  251. #[Rest\QueryParam(name: 'campaign', description: 'Campaign Name')]
  252. #[Rest\View(serializerGroups: ['Default', 'Details'])]
  253. #[OA\Parameter(name: 'campaign', in: 'query', description: 'Campaign Name', required: false)]
  254. #[OA\Parameter(name: 'contact_seller', in: 'query', description: '', required: false)]
  255. #[OA\Response(response: 404, description: 'Returned when the listing is not found')]
  256. public function contactSeller(Request $request, Listing $listing, ?User $user = null)
  257. {
  258. $currentUrl = $request->getUri();
  259. if (str_contains($currentUrl, 'v2')) {
  260. if (!$user = $this->getUser()) {
  261. throw new AccessDeniedHttpException();
  262. }
  263. }
  264. $form = $this->createForm(ContactSellerFormType::class, null, [
  265. 'method' => 'POST',
  266. 'csrf_protection' => false,
  267. ]);
  268. $form->handleRequest($request);
  269. if ($form->isSubmitted() && $form->isValid()) {
  270. // Get campaign name
  271. $campaign = $request->query->get('campaign');
  272. $message = $form->getData();
  273. // Send contact seller message and send a lead
  274. $composer = $this->messageComposer;
  275. $composer
  276. ->setSender($user)
  277. ->compose($message['message'], $listing, $campaign);
  278. return true;
  279. }
  280. return $form;
  281. }
  282. /**
  283. * @return Response $response
  284. *
  285. * @throws \Exception
  286. */
  287. #[Rest\Post('/api/v2/listing/{listing}/contact_seller/quick', options: ['i18n' => false], name: 'aqarmap_api_listing_quick_contact_seller_v2')]
  288. #[Rest\QueryParam(name: 'campaign', description: 'Campaign Name')]
  289. #[Rest\View(serializerGroups: ['Default', 'Details'])]
  290. #[OA\Parameter(name: 'campaign', in: 'query', description: 'Campaign Name', required: false)]
  291. #[OA\Parameter(name: 'contact_seller', in: 'query', description: '', required: false)]
  292. #[OA\Response(response: 201, description: 'Returned when user created and message sent successfully')]
  293. #[OA\Response(response: 404, description: 'Returned when the listing is not found')]
  294. #[OA\Response(response: 403, description: 'Returned when parameter is missing')]
  295. public function contactSellerWithQuickRegistration(Request $request, Listing $listing): Response
  296. {
  297. if (!$this->isValidQuickContactSellerParameters($request->get('contact_seller'))) {
  298. return new Response(null, Response::HTTP_FORBIDDEN);
  299. }
  300. $form = $this->createForm(QuickContactSellerType::class, null, [
  301. 'method' => 'POST',
  302. 'csrf_protection' => false,
  303. ]);
  304. $form->handleRequest($request);
  305. if ($form->isSubmitted() && !$form->isValid()) {
  306. return new Response(null, Response::HTTP_FORBIDDEN);
  307. }
  308. $userManager = $this->userManager;
  309. $fromData = $form->getData();
  310. $user = $userManager->createQuickUser($request, $form);
  311. $campaign = $request->query->get('campaign');
  312. $composer = $this->messageComposer;
  313. $composer
  314. ->setSender($user)
  315. ->compose($fromData['message'], $listing, $campaign);
  316. return new Response(null, Response::HTTP_CREATED);
  317. }
  318. private function isValidQuickContactSellerParameters(array $parameters): bool
  319. {
  320. if (
  321. !\array_key_exists('email', $parameters)
  322. || !\array_key_exists('phone', $parameters)
  323. || !\array_key_exists('countryCode', $parameters)
  324. ) {
  325. return false;
  326. }
  327. return true;
  328. }
  329. /**
  330. * @return bool|array
  331. */
  332. #[Rest\Post('/api/v2/listing/{listing}/call_request', options: ['i18n' => false], name: 'aqarmap_api_listing_call_request_v2')]
  333. #[Rest\QueryParam(name: 'campaign', description: 'Campaign Name')]
  334. #[Rest\View(serializerGroups: ['Default', 'Details'])]
  335. public function callRequest(Listing $listing, Request $request, ?User $user = null)
  336. {
  337. $version = (float) ltrim((string) $request->headers->get('X-Accept-Version'), 'v');
  338. $callRequest = new CallRequest();
  339. $currentUrl = $request->getUri();
  340. if (str_contains($currentUrl, 'v2')) {
  341. if (!$user = $this->getUser() && $version <= 2.13) {
  342. throw new AccessDeniedHttpException();
  343. }
  344. }
  345. $form = $this->createForm(QuickLeadType::class, null, [
  346. 'method' => 'POST',
  347. 'csrf_protection' => false,
  348. ]);
  349. $form->handleRequest($request);
  350. $lead = $form->getData();
  351. /** @var UserManagerInterface $userManager */
  352. $userManager = $this->fosUserManager;
  353. $aqarmapUserManager = $this->userManager;
  354. $hasEmail = !$lead['isAutoGeneratedEmail'];
  355. $phoneManager = $this->phoneManager;
  356. $originalPhoneNumber = $lead['phone'];
  357. $countryCode = $lead['countryCode'];
  358. $phoneNumber = $phoneManager->trimZero($lead['phone'], $countryCode);
  359. /** @var User $user */
  360. $user = $userManager->findUserByEmail($lead['email']);
  361. if (!$user && !$hasEmail) {
  362. $user = $aqarmapUserManager->findLatestByPhone($phoneNumber, $countryCode);
  363. }
  364. if (!$user && $hasEmail) {
  365. $user = $aqarmapUserManager->findAndReplaceUserAndEmail($phoneNumber, $lead['email'], $countryCode);
  366. }
  367. if (!$user) {
  368. $user = $userManager->createUser();
  369. $user
  370. ->setFullName($lead['name'])
  371. ->setPhoneNumber($lead['phone'])
  372. ->setTempOriginalPhoneNumber($originalPhoneNumber)
  373. ->setTempCountryCode($lead['countryCode'])
  374. ->setEmail($lead['email'])
  375. ->setHasEmail($hasEmail)
  376. ->setLanguage('ar');
  377. $user = $aqarmapUserManager->quickRegistration($form, $user, $request);
  378. }
  379. $phone = $phoneManager->addNewUserPhone($phoneNumber, $lead['countryCode'], $user, true, false, null, $originalPhoneNumber);
  380. $userManager->updateUser($user);
  381. $userManager->reloadUser($user);
  382. $callRequest->setPhone($phone->getPhone());
  383. $callRequest->setLeadFullName($request->request->get('name'));
  384. $callRequest->setLeadEmail($request->request->get('email'));
  385. $campaign = $request->request->get('campaign');
  386. $callRequest->setUser($user);
  387. $callRequest->setListing($listing);
  388. $this->callRequestManager->submitCallRequest($callRequest, $campaign);
  389. return true;
  390. }
  391. /**
  392. * Add New User Listing.
  393. *
  394. * @return View|Form
  395. */
  396. #[Rest\Post('/api/v2/listing', options: ['i18n' => false], name: 'aqarmap_api_add_listing')]
  397. #[IsGranted(attribute: 'ROLE_USER')]
  398. #[OA\Parameter(name: 'listing', in: 'query', description: '', required: false)]
  399. #[OA\Response(response: 201, description: 'Returned when successfully created')]
  400. #[OA\Response(response: 400, description: 'Returned when validation error')]
  401. public function addListing(Request $request)
  402. {
  403. $form = $this->createForm(ListingApiType::class, $listing = new Listing(), [
  404. 'csrf_protection' => false,
  405. ]);
  406. $form->handleRequest($request);
  407. if ($form->isSubmitted() && $form->isValid()) {
  408. if ($this->getUser()->getPhoneNumber()) {
  409. $listing->addPhone(new ListingPhone($this->getUser()->getPhoneNumber()));
  410. }
  411. $listingManager = $this->listingService;
  412. $listing->setUser($this->getUser());
  413. $listing->setStatus(ListingStatus::DRAFT);
  414. $listingManager->saveListing($listing);
  415. $listingEvent = new ListingEvent($listing);
  416. $this->dispatcher->dispatch($listingEvent, 'aqarmap.listing.submitted');
  417. return View::create()->setData($listing)->setStatusCode(Response::HTTP_CREATED);
  418. }
  419. return View::create()->setData($form)->setStatusCode(Response::HTTP_BAD_REQUEST);
  420. }
  421. /**
  422. * Create Single Listing Note.
  423. */
  424. #[Rest\Post('/api/v2/listing/{id}/note', options: ['i18n' => false])]
  425. #[Rest\RequestParam(name: 'body', description: 'Note Body')]
  426. #[Rest\RequestParam(name: 'source', description: 'Supported sources: 1 = Website, 2 = Consumer App, 3 = Live App')]
  427. #[Rest\RequestParam(name: 'last_modified_at', description: 'dateTime')]
  428. #[Rest\RequestParam(name: 'created_at', description: 'dateTime')]
  429. #[IsGranted(attribute: 'ROLE_USER')]
  430. public function postListingSingleNote(
  431. Request $request,
  432. #[MapEntity(id: 'listing')]
  433. Listing $listing,
  434. ListingNoteRepository $listingNoteRepository,
  435. EntityManagerInterface $em,
  436. ) {
  437. $currentUser = $this->getUser();
  438. $listingNote = $listingNoteRepository->findOneBy([
  439. 'user' => $currentUser,
  440. 'listing' => $listing,
  441. ]);
  442. if (empty($listingNote)) {
  443. $listingNote = new ListingNote();
  444. $listingNote->setUser($currentUser);
  445. $listingNote->setListing($listing);
  446. $listingNote->setCreatedAt(new \DateTime());
  447. }
  448. $form = $this->createForm(ListingNoteType::class, $listingNote);
  449. $form->handleRequest($request);
  450. if ($form->isSubmitted() && $form->isValid()) {
  451. $em->persist($listingNote);
  452. $em->flush();
  453. return $this->json([
  454. 'message' => 'Successfully submitted',
  455. ], Response::HTTP_CREATED);
  456. }
  457. return $this->json([
  458. 'message' => 'Error occurred',
  459. ], Response::HTTP_BAD_REQUEST);
  460. }
  461. /**
  462. * Create Bulk Listing Note.
  463. *
  464. * @return array
  465. */
  466. #[Rest\Post('/api/listing/note', options: ['i18n' => false])]
  467. #[Rest\Post('/api/v2/listing/note', options: ['i18n' => false])]
  468. #[IsGranted(attribute: 'ROLE_USER')]
  469. public function postListingBulkNote(Request $request)
  470. {
  471. $listingNoteService = $this->listingNoteService;
  472. $listingNoteService->addBulk($request);
  473. return $this->respond('Note Added Successfully!');
  474. }
  475. /**
  476. * Update Listing.
  477. *
  478. * @return View|Form
  479. */
  480. #[Rest\Post('/api/v2/listing/{id}', options: ['i18n' => false], name: 'aqarmap_api_update_listing')]
  481. #[Rest\View]
  482. #[IsGranted(attribute: 'ROLE_OWNER', subject: 'listing')]
  483. #[OA\Parameter(name: 'listing', in: 'query', description: '', required: false)]
  484. #[OA\Response(response: 200, description: 'Returned when successfully Updated')]
  485. #[OA\Response(response: 400, description: 'Returned when validation error')]
  486. public function updateListing(Listing $listing, Request $request)
  487. {
  488. $listing->clearAttributes();
  489. $form = $this->createForm(ListingApiType::class, $listing, [
  490. 'csrf_protection' => false,
  491. ]);
  492. $form->handleRequest($request);
  493. if ($form->isSubmitted() && $form->isValid()) {
  494. if ($this->getUser()->getPhoneNumber()) {
  495. $listing->addPhone(new ListingPhone($this->getUser()->getPhoneNumber()));
  496. }
  497. $listingManager = $this->listingService;
  498. $listingManager->saveListing($listing);
  499. $listingEvent = new ListingEvent($listing);
  500. $this->dispatcher->dispatch($listingEvent, 'aqarmap.listing.submitted');
  501. return View::create()->setData($listing)->setStatusCode(Response::HTTP_OK);
  502. }
  503. return View::create()->setData($form)->setStatusCode(Response::HTTP_BAD_REQUEST);
  504. }
  505. /**
  506. * Upload listing photos.
  507. *
  508. * @return View|array
  509. */
  510. #[Rest\Post('/api/v2/listing/{id}/photos', options: ['i18n' => false], name: 'aqarmap_api_upload_listing_photos')]
  511. #[IsGranted(attribute: 'ROLE_OWNER', subject: 'listing')]
  512. #[OA\Parameter(name: 'photos', in: 'query', description: '', required: false)]
  513. #[OA\Response(response: 201, description: 'Returned when successfully created')]
  514. #[OA\Response(response: 400, description: 'Returned when validation error')]
  515. public function upload(Request $request, Listing $listing)
  516. {
  517. $form = $this->createForm(PhotoType::class, null, [
  518. 'csrf_protection' => false,
  519. ]);
  520. $form->handleRequest($request);
  521. if ($form->isSubmitted() && $form->isValid()) {
  522. $outputFiles = [];
  523. $listingManager = $this->listingService;
  524. $maxOrder = $listingManager->getMaxListingPhotoOrder($listing);
  525. foreach ($form->get('file')->getData() as $index => $file) {
  526. $photo = new Photo();
  527. $photo->setFile($file);
  528. $listingPhoto = new ListingPhoto();
  529. $listingPhoto->setFile($photo);
  530. $listingPhoto->setCaption($photo->getFile()->getClientOriginalName());
  531. $listingPhoto->setOrder($maxOrder + $index + 1);
  532. $outputFiles[] = $listingPhoto;
  533. $listing->addPhoto($listingPhoto);
  534. }
  535. $listingEvent = new ListingEvent($listing);
  536. $this->dispatcher->dispatch($listingEvent, 'aqarmap.listing.submitted');
  537. $listingManager->saveListing($listing);
  538. return View::create()->setData($outputFiles)->setStatusCode(Response::HTTP_CREATED);
  539. }
  540. return [
  541. 'listing' => $listing,
  542. 'form' => $form->createView(),
  543. ];
  544. }
  545. /**
  546. * Delete Listing entity.
  547. */
  548. #[Rest\Delete('/api/v2/listing/{listing}', options: ['i18n' => false], name: 'aqarmap_api_delete_listing')]
  549. #[IsGranted(attribute: 'ROLE_OWNER', subject: 'listing')]
  550. #[OA\Response(response: 204, description: 'Returned when successfully Deleted')]
  551. #[OA\Response(response: 404, description: 'Returned when Listing is not found')]
  552. #[OA\Response(response: 403, description: 'Returned when you are trying to remove listing that not yours.')]
  553. public function delete(Listing $listing)
  554. {
  555. if (\in_array('ROLE_PREVENT_DELETE_LISTING', $this->getUser()->getRoles())) {
  556. throw new AccessDeniedHttpException("Forbidden, user don't have this permession.");
  557. }
  558. $listingManager = $this->listingService;
  559. $listingManager->remove($listing, ListingStatus::USER_DELETED);
  560. return new Response(null, Response::HTTP_NO_CONTENT);
  561. }
  562. /**
  563. * Undelete Listing entity.
  564. *
  565. * @return array
  566. */
  567. #[IsGranted(attribute: 'ROLE_OWNER', subject: 'listing')]
  568. #[Rest\View]
  569. #[Rest\Get('/api/v2/listing/{listing}/undelete', name: 'aqarmap_api_undelete_listing', options: ['i18n' => false])]
  570. #[OA\Response(response: 200, description: 'Returned when successfully undeleted')]
  571. #[OA\Response(response: 404, description: 'Returned when Listing is not found or is not on user deleted state')]
  572. #[OA\Response(response: 403, description: 'Returned when you are trying to remove listing that not yours.')]
  573. public function undelete(Listing $listing)
  574. {
  575. if (ListingStatus::USER_DELETED != $listing->getStatus()) {
  576. throw $this->createNotFoundException('Unable to find this listing.');
  577. }
  578. $listingManager = $this->listingService;
  579. $listingManager->changeStatus($listing, ListingStatus::PENDING);
  580. $listingEvent = new ListingEvent($listing);
  581. $this->dispatcher->dispatch($listingEvent, 'aqarmap.listing.resubmitted');
  582. return ['listing' => $listing];
  583. }
  584. /**
  585. * Republish Listing entity.
  586. *
  587. * @return array
  588. */
  589. #[IsGranted(attribute: 'ROLE_OWNER', subject: 'listing')]
  590. #[Rest\View]
  591. #[OA\Response(response: 200, description: 'Returned when successfully Republished')]
  592. #[OA\Response(response: 404, description: 'Returned when Listing is not found or is not on Expired state')]
  593. #[OA\Response(response: 403, description: 'Returned when you are trying to Republish listing that not yours.')]
  594. #[Rest\Get('/api/v2/listing/{listing}/relist', name: 'aqarmap_api_republish_listing', options: ['i18n' => false])]
  595. public function relist(Listing $listing)
  596. {
  597. if (ListingStatus::EXPIRED != $listing->getStatus()) {
  598. throw $this->createNotFoundException('Unable to find this listing.');
  599. }
  600. $em = $this->managerRegistry->getManager();
  601. $listingRepo = $em->getRepository(Listing::class);
  602. $relistChild = $listingRepo->getRelistChild($listing);
  603. // If the expired listing already republished before .. return the republished version.
  604. if ($relistChild) {
  605. return ['listing' => $relistChild];
  606. }
  607. $listingManager = $this->listingService;
  608. $listing = $listingManager->relist($listing);
  609. $listingEvent = new ListingEvent($listing);
  610. $this->dispatcher->dispatch($listingEvent, 'aqarmap.listing.resubmitted');
  611. return ['listing' => $listing];
  612. }
  613. /**
  614. * Feature A Listing.
  615. *
  616. * @return View
  617. */
  618. #[Rest\Post('/api/v2/listing/{listing}/feature', options: ['i18n' => false], name: 'aqarmap_api_feature_listing')]
  619. #[Rest\RequestParam(name: 'type', description: 'Feature Type, 1 for payment, 2 for making it featured')]
  620. #[IsGranted(attribute: 'ROLE_OWNER', subject: 'listing')]
  621. public function feature(Listing $listing, Request $request)
  622. {
  623. $type = $request->get('type');
  624. if (!$type) {
  625. throw new BadRequestHttpException('Please Specify the feature type');
  626. }
  627. $matcher = $this->listingRuleMatcher;
  628. $listingRule = $matcher->match($listing);
  629. $translator = $this->translator;
  630. $featuredDuration = $listingRule['featured_duration'];
  631. $featuredFees = $listingRule['featured_fees'];
  632. if (ListingFeatures::FEATURED == $type) {
  633. // Check if there is a value for featured_duration & featured_fees
  634. if (empty($featuredDuration) || empty($featuredFees)) {
  635. throw new LogicHttpException($translator->trans('Making a listing Featured is not available for this listing.', [], 'exceptions'));
  636. }
  637. try {
  638. $this->listingService->makeItFeatured(
  639. $listing,
  640. [
  641. 'featuredFees' => $featuredFees,
  642. 'featuredDuration' => $featuredDuration,
  643. 'listingFeaturedType' => ListingFeaturedTypes::FEATURED,
  644. 'listingFeature' => $type,
  645. ]
  646. );
  647. } catch (\Exception $e) {
  648. throw new LogicHttpException($translator->trans($e->getMessage(), [], 'exceptions'));
  649. }
  650. } elseif (ListingFeatures::PAID == $type) {
  651. /** @var \Aqarmap\Bundle\CreditBundle\Services\CreditManager $creditManager */
  652. $creditManager = $this->creditManager;
  653. if ($listing->getPublicationCredit()) {
  654. throw new LogicHttpException($translator->trans('credit.already_paid'));
  655. } elseif ($listingRule['publication_fees'] > $creditManager->getBalance($listing->getUser())) {
  656. throw new LogicHttpException($translator->trans('credit.not_enough_credits'));
  657. }
  658. // Subtract publication fees
  659. $credits = $creditManager->deduction($listing->getUser(), $listingRule['publication_fees'], 'Listing Fees', CreditStatus::PENDING);
  660. foreach ($credits as $credit) {
  661. if ($credit instanceof Credit) {
  662. $this->listingService->addFeature($listing, ListingFeatures::PAID, null, $credit);
  663. }
  664. }
  665. $listingEvent = new ListingEvent($listing);
  666. $this->dispatcher->dispatch($listingEvent, 'aqarmap.listing.submitted');
  667. }
  668. return View::create()->setData(['listing' => $listing]);
  669. }
  670. // -------------------------------------------------------------------------//
  671. // ++++++++++++++ Quick contact seller & call request Actions +++++++++++++++//
  672. // -------------------------------------------------------------------------//
  673. /**
  674. * ).
  675. *
  676. * @return bool|Form|array
  677. *
  678. * @throws AccessDeniedHttpException
  679. *
  680. * ===============================================================
  681. * |
  682. * | DEPRECATED: USE QuickCreateLeadAction::LeadController instead
  683. * |
  684. * ===============================================================
  685. */
  686. #[Rest\Post('/api/v2/listing/{listing}/quick_create_lead', options: ['i18n' => false], name: 'aqarmap_api_listing_quick_create_lead')]
  687. #[Rest\QueryParam(name: 'campaign', description: 'Campaign Name')]
  688. #[Rest\QueryParam(name: 'country_code', default: null, description: 'Country Code')]
  689. #[Rest\View(serializerGroups: ['Default', 'Details'])]
  690. #[OA\Response(response: 404, description: 'Returned when the listing is not found')]
  691. public function quickCreateLead(Request $request, Listing $listing)
  692. {
  693. $form = $this->createForm(QuickCreateLeadFormType::class, null, [
  694. 'method' => 'POST',
  695. 'csrf_protection' => false,
  696. ]);
  697. $form->handleRequest($request);
  698. if ($form->isSubmitted() && $form->isValid()) {
  699. $lead = $form->getData();
  700. /** @var UserManagerInterface $userManager */
  701. $userManager = $this->fosUserManager;
  702. $leadModel = new LeadModel();
  703. try {
  704. /** @var User $user */
  705. if ($user = $userManager->findUserByEmail($form->get('email')->getData())) {
  706. $phone = $this->phoneManager->addNewUserPhone(
  707. $lead['phoneNumber'],
  708. '+'.str_replace(' ', '', $request->get('country_code')),
  709. $user,
  710. true,
  711. true,
  712. CountryCodes::getCountryFromCodeNumber('+'.str_replace(' ', '', $request->get('country_code'))),
  713. $lead['phoneNumber']
  714. );
  715. $userManager->updateUser($user);
  716. $leadModel->setUser($user);
  717. } else {
  718. $user = $userManager->createUser();
  719. $user
  720. ->setFullName($lead['fullName'])
  721. ->setEmail($lead['email'])
  722. ->setLanguage($request->getLocale());
  723. $user = $this->userManager->quickRegistration($form, $user, $request);
  724. $phone = $this->phoneManager->addNewUserPhone(
  725. $lead['phoneNumber'],
  726. '+'.str_replace(' ', '', $request->get('country_code')),
  727. $user,
  728. true,
  729. true,
  730. CountryCodes::getCountryFromCodeNumber('+'.str_replace(' ', '', $request->get('country_code'))),
  731. $lead['phoneNumber']
  732. );
  733. $userManager->reloadUser($user);
  734. $leadModel->setUser($user);
  735. }
  736. } catch (\Exception) {
  737. return [
  738. 'status' => 'error',
  739. 'message' => 'Cannot create a user.',
  740. ];
  741. }
  742. if ($user) {
  743. try {
  744. $leadModel->setEmail($lead['email']);
  745. $leadModel->setName($lead['fullName']);
  746. $leadModel->setPhone($phone->getPhone());
  747. $leadModel->setCampaign($request->request->get('campaign'));
  748. $leadModel->setMessage($form->get('message')->getData());
  749. $leadModel->setListing($listing);
  750. $this->listingService->quickCreateLead($leadModel);
  751. return [
  752. 'status' => 'ok',
  753. ];
  754. } catch (\Exception) {
  755. return [
  756. 'status' => 'error',
  757. 'message' => 'Cannot create the lead.',
  758. ];
  759. }
  760. }
  761. }
  762. return $form;
  763. }
  764. /**
  765. * Get Listing Rules.
  766. *
  767. * )
  768. *
  769. * @return array
  770. */
  771. #[Rest\Get('/api/v2/listing/{id}/rules', options: ['i18n' => false], name: 'aqarmap_api_get_listing_rules')]
  772. #[IsGranted(attribute: 'ROLE_OWNER', subject: 'listing')]
  773. #[Rest\View]
  774. #[OA\Response(response: 404, description: 'Returned when the listing is not found')]
  775. public function getListingRules(Listing $listing)
  776. {
  777. $listingMatcher = $this->listingRuleMatcher;
  778. $rules = $listingMatcher->match($listing);
  779. return ['rules' => $rules];
  780. }
  781. /**
  782. * Get Listing Note.
  783. *
  784. * @return array
  785. */
  786. #[Rest\Get('/api/v2/listing/{id}/note', options: ['i18n' => false], name: 'aqarmap_api_get_listing_note')]
  787. #[IsGranted(attribute: 'ROLE_USER')]
  788. #[Rest\View]
  789. #[OA\Parameter(name: 'propertyType', in: 'query', description: 'propertyType of the unites', required: false)]
  790. #[OA\Parameter(name: 'section', in: 'query', description: 'Section of the unites', required: false)]
  791. #[OA\Response(response: 404, description: 'Returned when the listing is not found')]
  792. public function getListingNote(Listing $listing)
  793. {
  794. /** @var EntityManager $em */
  795. $em = $this->managerRegistry->getManager();
  796. $listingNoteRepo = $em->getRepository(ListingNote::class);
  797. return $listingNoteRepo->findOneBy([
  798. 'user' => $this->getUser(),
  799. 'listing' => $listing,
  800. ]);
  801. }
  802. /**
  803. * Get account statistics (Listings Statistics).
  804. *
  805. * @throws \Exception
  806. */
  807. #[Rest\Get('/api/user/listings/statistics', requirements: ['id' => '\d+'], options: ['expose' => true, 'i18n' => false], name: 'aqarmap_api_get_user_listings_rates_counts')]
  808. #[Rest\Post('/api/v2/user/listings/statistics', options: ['i18n' => false], name: 'aqarmap_api_get_user_listings_rates_counts_v2')]
  809. #[Rest\View(serializerGroups: ['Default', 'Details'])]
  810. #[IsGranted(attribute: 'ROLE_USER')]
  811. public function getUserListingsPerformanceStatistics(
  812. Request $request,
  813. #[MapEntity(id: 'user')]
  814. ?User $user = null,
  815. ): View {
  816. $isSubAccount = null != $user && $this->getUser() === $user->getParent();
  817. $startDate = null;
  818. $period = $request->query->get('period', null);
  819. if (!$this->getUser() && !$isSubAccount) {
  820. throw new AccessDeniedHttpException();
  821. }
  822. $user = $user ?: $this->getUser();
  823. if ('7Days' == $period) {
  824. $startDate = date('Y-m-d', strtotime('-7 days'));
  825. } elseif ('30Days' == $period) {
  826. $startDate = date('Y-m-d', strtotime('-30 days'));
  827. }
  828. return $this->respond([
  829. 'rates' => $this->listingService->getUserListingsPerformanceStatistics($user, $startDate, $period),
  830. ]);
  831. }
  832. /**
  833. * Generates XML file for Listings News feed for marketing purpose.
  834. *
  835. * @return Response
  836. */
  837. #[Rest\Get('/api/listings/feed/{platform}', options: ['i18n' => false], name: 'aqarmap_api_get_listings_news_feed', defaults: ['page' => 1])]
  838. #[Rest\View(serializerGroups: ['Default', 'Details'])]
  839. public function getNewsFeed($platform, Request $request)
  840. {
  841. $criteria = [];
  842. $page = min($request->query->getInt('page') ?: 1, ListingNewsFeed::MAX_PAGES);
  843. $limit = min($request->query->getInt('limit', 100), 100);
  844. if ((int) $request->query->getInt('page', 1) > $page) {
  845. throw new NotFoundHttpException('Exceeded maximum number of pages');
  846. }
  847. if ($request->query->get('ugc')) {
  848. $data = explode(',', $request->query->get('ugc'));
  849. $data = array_map('intval', $data);
  850. $criteria['groupCategory'] = array_filter($data);
  851. }
  852. if ($request->query->get('ug')) {
  853. $data = explode(',', $request->query->get('ug'));
  854. $data = array_map('intval', $data);
  855. $criteria['userGroup'] = array_filter($data);
  856. }
  857. if ($request->query->get('hl')) {
  858. $criteria['locale'] = $request->query->get('hl');
  859. }
  860. if ($request->query->get('maxlead')) {
  861. $criteria['maxlead'] = $request->query->get('maxlead');
  862. }
  863. if ($request->query->get('feat')) {
  864. $criteria['feat'] = $request->query->get('feat');
  865. }
  866. if ($request->query->get('location')) {
  867. $locations = $this->locationManager
  868. ->buildLocationsArrayByParentId($request->query->get('location'));
  869. if (!empty($locations)) {
  870. $criteria['locations'] = $locations;
  871. }
  872. }
  873. if ($request->query->get('section')) {
  874. $data = explode(',', $request->query->get('section'));
  875. $data = array_map('intval', $data);
  876. $criteria['sections'] = array_filter($data);
  877. }
  878. $news = $this->listingNewsFeed;
  879. $offset = ($page - 1) * $limit;
  880. $excludedKeys = ['sections', 'maxlead'];
  881. if (!array_diff(array_keys($criteria), $excludedKeys)) {
  882. throw new BadRequestHttpException(sprintf('Invalid Request: Please add at least one additional filter besides %s.', implode(', ', $excludedKeys)));
  883. }
  884. if ('json' == $request->query->get('format')) {
  885. return $this->responseJson($news->asJson($offset, $limit, $criteria));
  886. }
  887. return $this->responseXml($news->asXml($offset, $limit, $platform, $criteria));
  888. }
  889. /**
  890. * Stateful endpoint to update listing fields.
  891. */
  892. #[Rest\Post('api/listing/{id}/edit', name: 'update_listing_field', options: ['i18n' => false, 'expose' => true])]
  893. #[Rest\View(serializerGroups: ['Default', 'Details'])]
  894. #[IsGranted(attribute: 'ROLE_EDIT_REVIEW_LISTINGS')]
  895. #[IsGranted(attribute: new Expression('is_granted("ROLE_ADMIN") or is_granted("ROLE_OWNER", subject["listing"])'), subject: ['listing'])]
  896. public function update(Listing $listing, Request $request): View
  897. {
  898. $listingManager = $this->listingManager;
  899. $isEnabled = $this->featureToggle
  900. ->isEnabled('web.mortgage.options');
  901. $mortgage = $request->get('eligibleForMortgage');
  902. $fields = $request->request->all();
  903. if ($isEnabled) {
  904. if (true == $mortgage) {
  905. $this->mortgageService->addEligibleMortgageTypes($listing);
  906. } else {
  907. $listingManager->setEligibleMortgageToNull($listing);
  908. }
  909. unset($fields['eligibleForMortgage']);
  910. }
  911. $this->dispatcher->dispatch(
  912. new ListingUpdatedEvent($listing, $fields),
  913. ListingUpdatedEvent::UPDATED
  914. );
  915. $listing = $listingManager->update($listing, $fields);
  916. if (ListingStatus::LIVE == $listing->getStatus()) {
  917. $listingManager->changeStatus($listing, ListingStatus::PENDING);
  918. }
  919. return $this->respond([
  920. 'listing' => $listing,
  921. 'msg' => 'Listing was updated successfully',
  922. ]);
  923. }
  924. /**
  925. * Stateful endpoint to get Photos Of The Given Listings.
  926. */
  927. #[Rest\Get('/api/listings/{listing}/photos', name: 'aqarmap_api_get_listing_photos', options: ['expose' => true, 'i18n' => false])]
  928. #[IsGranted(attribute: new Expression('is_granted("ROLE_ADMIN") or is_granted("ROLE_OWNER", subject["listing"])'), subject: ['listing'])]
  929. public function getListingPhotos(Listing $listing): View
  930. {
  931. $listingPhotos = $listing->getPhotos();
  932. $listingPhotosData = [];
  933. foreach ($listingPhotos as $listingPhoto) {
  934. $listingPhotosData[] = [
  935. 'id' => $listingPhoto->getId(),
  936. 'type' => $listingPhoto->getType(),
  937. 'caption' => $listingPhoto->getCaption(),
  938. 'file' => $listingPhoto->getFile(),
  939. ];
  940. }
  941. return $this->respond($listingPhotosData);
  942. }
  943. /**
  944. * Get Photo Types.
  945. *
  946. * @return Response
  947. */
  948. #[Rest\Get('/api/listings/photoTypes', name: 'aqarmap_api_get_listing_photo_types', options: ['expose' => true, 'i18n' => false])]
  949. public function getPhotoTypes()
  950. {
  951. return $this->respond(PhotoTypes::getPhotoTypes());
  952. }
  953. /**
  954. * Upload Listing Photo.
  955. *
  956. * @return array
  957. *
  958. * @throws OptimisticLockException
  959. */
  960. #[Rest\Post('/api/listing/{id}/upload_photo', name: 'aqarmap_api_admin_upload_listing_photo', options: ['expose' => true, 'i18n' => false])]
  961. public function uploadListingPhoto(Listing $listing)
  962. {
  963. $outputFiles = [];
  964. $listingManager = $this->listingManager;
  965. $maxOrder = $listingManager->getMaxListingPhotoOrder($listing);
  966. $photo = new Photo();
  967. $file = $_FILES['file'];
  968. $file = new UploadedFile($file['tmp_name'], $file['name'], $file['type']);
  969. $photo->setFile($file);
  970. $listingPhoto = new ListingPhoto();
  971. $listingPhoto->setFile($photo);
  972. $listingPhoto->setCaption($photo->getFile()->getClientOriginalName());
  973. $listingPhoto->setOrder($maxOrder + 1);
  974. $outputFiles[] = $listingPhoto;
  975. $listing->addPhoto($listingPhoto);
  976. $em = $this->managerRegistry->getManager();
  977. $em->persist($listing);
  978. $em->flush();
  979. return $this->respond(['status' => 'OK']);
  980. }
  981. /**
  982. * Gets rate details of listing.
  983. */
  984. #[Rest\Get('/api/v2/listing/{listing}/rates', options: ['i18n' => false])]
  985. #[Rest\Get('/api/listing/{listing}/rates', options: ['expose' => true, 'i18n' => false], name: 'aqarmap_api_listing_rates_details')]
  986. #[Rest\View(serializerGroups: ['Rates'])]
  987. #[Cache(expires: '+1 week', maxage: '+1 week', smaxage: '+1 week', public: false, vary: ['Accept-Language', 'X-Accept-Version', 'Accept'])]
  988. public function getRateDetails(Listing $listing)
  989. {
  990. return $this->respond(
  991. current($this->listingRateService->getRatesDetails([$listing]))
  992. );
  993. }
  994. /**
  995. * @return View
  996. */
  997. #[Rest\Get('/api/v2/listing/{listing}/preview', options: ['i18n' => false])]
  998. #[Rest\Get('/api/listing/{listing}/preview', options: ['expose' => true, 'i18n' => false], name: 'aqarmap_api_listing_preview_data')]
  999. #[Rest\View(serializerGroups: ['Preview'])]
  1000. public function getListingPreviewData(Listing $listing)
  1001. {
  1002. return $this->respond($listing);
  1003. }
  1004. /**
  1005. * @return JsonResponse
  1006. *
  1007. * @throws \Exception
  1008. */
  1009. #[Rest\Get('/api/listing/{listing}/lead-analytics', options: ['i18n' => false, 'expose' => true], name: 'aqarmap_api_listing_lead_analytics')]
  1010. public function getLeadAnalytics(Listing $listing)
  1011. {
  1012. $analytics = $this->listingService->getLeadAnalytics($listing);
  1013. return $this->respond($analytics);
  1014. }
  1015. /**
  1016. * @return string
  1017. */
  1018. #[Rest\Delete('/api/v2/listing/{listing}/favourite', options: ['i18n' => false])]
  1019. #[Rest\Delete('/api/listing/{listing}/favourite', options: ['i18n' => false], name: 'aqarmap_api_remove_favourite_listing')]
  1020. #[IsGranted(attribute: 'ROLE_USER')]
  1021. public function deleteFavourite(Request $request)
  1022. {
  1023. $this->favouriteService->deleteByListing($request->attributes->get('listing'));
  1024. return $this->respond('Favourite Deleted Successfully!');
  1025. }
  1026. /**
  1027. * @return string
  1028. */
  1029. #[Rest\Delete('/api/v2/listing/{listing}/note', options: ['i18n' => false])]
  1030. #[Rest\Delete('/api/listing/{listing}/note', options: ['i18n' => false], name: 'aqarmap_api_remove_listing_note')]
  1031. #[IsGranted(attribute: 'ROLE_USER')]
  1032. public function deleteNote(Request $request)
  1033. {
  1034. $this->listingNoteService->deleteByListing($request->attributes->get('listing'));
  1035. return $this->respond('Note Deleted Successfully!');
  1036. }
  1037. #[Rest\Post('/api/listing/{listing}/rate-review', options: ['i18n' => false, 'expose' => true], name: 'aqarmap_api_change_listing_rate_review_status')]
  1038. #[IsGranted(attribute: 'ROLE_ADMIN')]
  1039. public function rateReview(Request $request, Listing $listing): View
  1040. {
  1041. $this->listingService->updateIsRateReviewed($listing, $request->request->get('isReviewed'));
  1042. return $this->respond('Listing rate review status changed successfully');
  1043. }
  1044. #[Rest\Get('/api/listing/{id}/similar_listings_count', options: ['i18n' => false, 'expose' => true], name: 'aqarmap_api_get_similar_listings_count')]
  1045. #[Rest\View(serializerGroups: ['Default', 'Details'])]
  1046. #[OA\Response(response: 404, description: 'Returned when the listing is not found')]
  1047. public function getSimilarListingsCount(Listing $listing): int
  1048. {
  1049. $listingManager = $this->listingService;
  1050. try {
  1051. $count = $listingManager->countSimilarListings($listing);
  1052. } catch (\Exception $exception) {
  1053. echo $exception->getMessage();
  1054. $this->errorLogger->error(shell_exec("Error retriving similar listings count! {$exception->getMessage()}"));
  1055. $count = 0;
  1056. }
  1057. return $count;
  1058. }
  1059. #[Rest\Get('/api/v2/listings/notes', options: ['i18n' => false])]
  1060. #[Rest\Get('/api/listings/notes', name: 'aqarmap_api_listing_notes_and_favourites', options: ['i18n' => false])]
  1061. #[Rest\QueryParam(name: 'from', description: '[favourites|notes]')]
  1062. #[Rest\View(serializerGroups: ['Default', 'List'])]
  1063. public function getListingsWithNotes(Request $request): array
  1064. {
  1065. $listingsIdWithNotes = [];
  1066. if ($content = $request->getContent()) {
  1067. $listingsIdWithNotes = json_decode($content, true);
  1068. }
  1069. $listings = $this->managerRegistry->getRepository(Listing::class)
  1070. ->getByIdsQuery(array_column($listingsIdWithNotes, 'listingId'));
  1071. $pagination = $this->paginator->paginate(
  1072. $listings,
  1073. $request->query->getInt('page', 1),
  1074. $request->query->getInt('limit', 10)
  1075. );
  1076. $data = $this->listingService->setStaticNotesFromPaginator($pagination, $listingsIdWithNotes);
  1077. if ('favourites' == $request->query->get('from')) {
  1078. return ['favourite' => $data];
  1079. }
  1080. return ['note' => $data];
  1081. }
  1082. /**
  1083. * ApiDoc(
  1084. * resource = true,
  1085. * section = "Listing",
  1086. * description="Get ApprovalRejectionWaitingTime",
  1087. * statusCodes={
  1088. * 200="Returned when successful",
  1089. * }
  1090. * ).
  1091. *
  1092. * @return array
  1093. *
  1094. * @throws \Exception
  1095. */
  1096. #[Rest\Get('/api/listing/arwt', options: ['i18n' => false], name: 'aqarmap_api_listing_arwt')]
  1097. #[Rest\View]
  1098. public function getApprovalRejectionWaitingTime(Request $request)
  1099. {
  1100. $criteria = array_merge(
  1101. $request->query->all(),
  1102. ['actionTime' => true]
  1103. );
  1104. $listingManager = $this->listingService;
  1105. $listingExtension = $this->listingExtension;
  1106. return [
  1107. 'arwt' => $listingExtension->waited($listingManager->getApprovalRejectionWaitingTime($criteria)),
  1108. ];
  1109. }
  1110. /**
  1111. * filter Listing Children with property type (Project Units).
  1112. *
  1113. * @return array
  1114. */
  1115. #[Rest\Get('/api/listing/{id}/children', name: 'aqarmap_api_get_listing_children', options: ['i18n' => false])]
  1116. #[Rest\QueryParam(name: 'propertyType', description: 'propertyType of the unites')]
  1117. #[Rest\View(serializerGroups: ['UnitDetails'])]
  1118. #[OA\Parameter(name: 'propertyType', description: 'propertyType of the unites', in: 'query', required: false)]
  1119. #[OA\Response(response: 404, description: 'Returned when the listing is not found')]
  1120. public function filterListingChildren(
  1121. Listing $listing,
  1122. #[MapEntity(id: 'propertyType')]
  1123. ?PropertyType $propertyType = null,
  1124. ): View {
  1125. if (!$this->getUser()) {
  1126. throw new AccessDeniedHttpException();
  1127. }
  1128. return $this->respond($listing->getLiveChildren($propertyType));
  1129. }
  1130. /**
  1131. * API for cloning listing.
  1132. */
  1133. #[Rest\Post('/api/listing/{id}/clone', name: 'aqarmap_api_clone_listing_children', options: ['i18n' => false])]
  1134. #[Rest\Post('/api/v2/listing/{id}/clone', name: 'aqarmap_api_v2_clone_listing_children', options: ['i18n' => false])]
  1135. #[Rest\RequestParam(name: 'propertyType', description: 'propertyType of the unites')]
  1136. #[Rest\RequestParam(name: 'section', description: 'Section of the unites')]
  1137. #[IsGranted(attribute: 'IS_AUTHENTICATED_REMEMBERED')]
  1138. #[Rest\View(serializerGroups: ['UnitDetails'])]
  1139. #[OA\Parameter(name: 'propertyType', description: 'propertyType of the unites', in: 'query', required: false)]
  1140. #[OA\Parameter(name: 'section', description: 'Section of the unites', in: 'query', required: false)]
  1141. #[OA\Response(response: 404, description: 'Returned when the listing, propertyType or section is not found')]
  1142. #[ParamConverter('propertyType', class: PropertyType::class, options: ['mapping' => ['propertyType' => 'id']], converter: 'querystring')]
  1143. #[ParamConverter('section', class: Section::class, options: ['mapping' => ['section' => 'id']], converter: 'querystring')]
  1144. public function cloneListing(
  1145. Listing $sourceListing,
  1146. PropertyType $propertyType,
  1147. Section $section,
  1148. ): View {
  1149. $syncedFields = $sourceListing->getParent() ? ListingSyncedFields::UNIT_FIELDS : ListingSyncedFields::PARENT_FIELDS;
  1150. try {
  1151. $listing = $this->listingManager->initializeListingForCloning($sourceListing, $propertyType, $section, $this->getUser());
  1152. $this->listingManager->syncListingForProject(
  1153. [
  1154. 'targetListingId' => $listing->getId(),
  1155. 'sourceListingId' => $sourceListing->getId(),
  1156. 'syncedFields' => $syncedFields,
  1157. 'targetListingSavedAt' => $listing->getUpdatedAt()->getTimestamp(),
  1158. ]
  1159. );
  1160. return $this->respond($listing->getId());
  1161. } catch (\Exception $exception) {
  1162. throw new \Exception('Could not clone listing', null, $exception);
  1163. }
  1164. }
  1165. #[Rest\Get('/api/v2/listing/{listing}/top-seller', options: ['i18n' => false], name: 'aqarmap_api_listing_top_seller')]
  1166. #[Rest\View(serializerGroups: ['TopCustomers'])]
  1167. public function getListingTopSeller(Listing $listing, TopSellerRetrievalService $topSellerRetrievalService)
  1168. {
  1169. $topSeller = new TopSeller();
  1170. $topSeller->setLocation($listing->getLocation()->getId());
  1171. $topSeller->setSection($listing->getSection()->getId());
  1172. $topSeller->setPropertyType($listing->getPropertyType()->getId());
  1173. return $topSellerRetrievalService->getTopSellerPersonalData($topSeller);
  1174. }
  1175. #[Rest\Get('/api/v2/listing/{listing}/related-listings', name: 'aqarmap_api_listing_related_listings', options: ['i18n' => false])]
  1176. #[Cache(expires: '+1 days', maxage: '+1 days', smaxage: '+1 days', public: true, vary: ['Accept-Language', 'X-Accept-Version', 'Accept'])]
  1177. #[Rest\View(serializerGroups: ['RelatedListingsV2'])]
  1178. public function getRelatedListing(Listing $listing, ListingManager $listingManager, ListingRepository $listingRepository)
  1179. {
  1180. try {
  1181. $relatedListingsElasticResponse = $listingManager->getRelatedListingsElasticResponse($listing);
  1182. $listingsQueryBuilder = $listingRepository->getListingsByIds(array_column($relatedListingsElasticResponse['items'], 'id'));
  1183. return $listingsQueryBuilder->getQuery()->getResult();
  1184. } catch (\Exception $exception) {
  1185. throw new \Exception('Could not get related listings', null, $exception);
  1186. }
  1187. }
  1188. #[Rest\Post('/api/v2/listing/{id}/views', name: 'aqarmap_api_add_listing_views', requirements: ['id' => '\d+'], options: ['i18n' => false])]
  1189. #[Rest\View]
  1190. public function increaseListingViews(Listing $listing): array
  1191. {
  1192. try {
  1193. $this->interactionService->increaseViews($listing, $this->getUser());
  1194. return [
  1195. 'status' => 'ok',
  1196. 'message' => 'Views Created Successfully!',
  1197. ];
  1198. } catch (\Exception $exception) {
  1199. throw new \Exception('Could not increase listing views', null, $exception);
  1200. }
  1201. }
  1202. }