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