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

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