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