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

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