src/Controller/Account/ProfileManagementController.php line 107

Open in your IDE?
  1. <?php
  2. /**
  3.  * Created by simpson <simpsonwork@gmail.com>
  4.  * Date: 2019-08-23
  5.  * Time: 17:02
  6.  */
  7. namespace App\Controller\Account;
  8. use AngelGamez\PorpaginasBundle\Controller\PaginationTrait;
  9. use App\Controller\RefererTrait;
  10. use App\Entity\Profile\Genders;
  11. use App\Entity\Profile\Profile;
  12. use App\Entity\Sales\Profile\TopPlacement;
  13. use App\Form\BulkActionForm;
  14. use App\PaymentProcessing\Exception\NotEnoughMoneyException;
  15. use App\Repository\ModerationRequestsRepository;
  16. use App\Repository\ProfileRepository;
  17. use App\Repository\ProfileTopPlacementRepository;
  18. use App\Service\BulkQueueService;
  19. use App\Service\BulkService;
  20. use App\Service\CaptchaService;
  21. use App\Service\Features;
  22. use App\Service\ProfileCtrService;
  23. use App\Service\ModerationService;
  24. use App\Service\MoneyFormatterService;
  25. use App\Service\PlacementHider;
  26. use App\Service\ProfileAdBoard;
  27. use App\Service\ProfileChargesCalculator;
  28. use App\Service\ProfilePlanPlacementService;
  29. use App\Service\ProfileRemover;
  30. use App\Service\ProfileTopBoard;
  31. use App\Service\TimeZoneService;
  32. use Doctrine\Persistence\ManagerRegistry;
  33. use Doctrine\ORM\EntityManagerInterface;
  34. use FOS\ElasticaBundle\Persister\PersisterRegistry;
  35. use Sensio\Bundle\FrameworkExtraBundle\Configuration\IsGranted;
  36. use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
  37. use Symfony\Component\DependencyInjection\ParameterBag\ParameterBagInterface;
  38. use Symfony\Component\HttpFoundation\JsonResponse;
  39. use Symfony\Component\HttpFoundation\RedirectResponse;
  40. use Symfony\Component\HttpFoundation\Request;
  41. use Symfony\Component\HttpFoundation\RequestStack;
  42. use Symfony\Component\HttpFoundation\Response;
  43. use Symfony\Component\HttpFoundation\Session\SessionInterface;
  44. use Symfony\Component\HttpKernel\KernelInterface;
  45. use Symfony\Component\Routing\Annotation\Route;
  46. use Symfony\Component\Security\Csrf\CsrfToken;
  47. use Symfony\Component\Security\Csrf\CsrfTokenManagerInterface;
  48. class ProfileManagementController extends AbstractController
  49. {
  50.     use RefererTrait;
  51.     use PaginationTrait;
  52.     use AccountListFilterTrait;
  53.     const LEGACY_MASSEUR_LIST_PARAM 'massagers';
  54.     private CsrfTokenManagerInterface $csrfTokenManager;
  55.     private ProfileTopPlacementRepository $profileTopPlacementRepository;
  56.     private MoneyFormatterService $moneyFormatterService;
  57.     private TimeZoneService $timeZoneService;
  58.     private ProfileChargesCalculator $profileChargesCalculator;
  59.     private ModerationService $moderationService;
  60.     private ModerationRequestsRepository $moderationRequestsRepository;
  61.     private KernelInterface $kernel;
  62.     private BulkQueueService $bulkQueueService;
  63.     private Features $features;
  64.     function __construct(
  65.         CsrfTokenManagerInterface $csrfTokenManagerProfileTopPlacementRepository $profileTopPlacementRepository,
  66.         MoneyFormatterService $moneyFormatterService,
  67.         RequestStack $requestStackTimeZoneService $timeZoneServiceProfileChargesCalculator $profileChargesCalculator,
  68.         Features $featuresModerationService $moderationServiceModerationRequestsRepository $moderationRequestsRepository,
  69.         SessionInterface $sessionParameterBagInterface $parameterBagKernelInterface $kernelBulkQueueService $bulkQueueService,
  70.         ProfileRepository $profileRepository
  71.     )
  72.     {
  73.         $this->csrfTokenManager $csrfTokenManager;
  74.         $this->profileTopPlacementRepository $profileTopPlacementRepository;
  75.         $this->moneyFormatterService $moneyFormatterService;
  76.         $this->request $requestStack->getCurrentRequest();
  77.         $this->timeZoneService $timeZoneService;
  78.         $this->profileChargesCalculator $profileChargesCalculator;
  79.         $this->moderationService $moderationService;
  80.         $this->moderationRequestsRepository $moderationRequestsRepository;
  81.         $this->session $session;
  82.         $this->parameterBag $parameterBag;
  83.         $this->availablePlacementTypeFilters AccountController::AVAILABLE_PROFILE_PLACEMENT_TYPES;
  84.         $this->kernel $kernel;
  85.         $this->bulkQueueService $bulkQueueService;
  86.         $this->profileRepository $profileRepository;
  87.         $this->features $features;
  88.     }
  89.     /**
  90.      * @throws \Doctrine\ORM\NoResultException
  91.      * @throws \Doctrine\ORM\NonUniqueResultException
  92.      */
  93.     #[Route(path'/'name'account_girls')]
  94.     #[Route(path'/page{page<\d+>}/'name'account_girls._pagination')]
  95.     #[Route(path'/'name'account.profile_management.list')]
  96.     #[Route(path'/page{page<\d+>}/'name'account.profile_management.list._pagination'options: ['expose' => true])]
  97.     #[Route(path'/masseur/'name'account.profile_management.list_masseur')]
  98.     #[Route(path'/masseur/page{page<\d+>}/'name'account.profile_management.list_masseur._pagination'options: ['expose' => true])]
  99.     #[IsGranted('ROLE_ADVERTISER')]
  100.     public function list(
  101.         Request $requestProfileRepository $profileRepositoryParameterBagInterface $parameterBag,
  102.         ProfileCtrService $profileCtrServiceBulkQueueService $bulkQueueServiceProfilePlanPlacementService $planPlacementService
  103.     ): Response
  104.     {
  105.         $user $this->getUser();
  106.         $isMasseurList $request->query->has(self::LEGACY_MASSEUR_LIST_PARAM)
  107.             || 'account.profile_management.list_masseur' === $request->attributes->get('_route')
  108.             || 'account.profile_management.list_masseur._pagination' === $request->attributes->get('_route');
  109.         list(
  110.             'per_page' => $perPage'placement_type_filter' => $placementTypeFilter'name_filter' =>  $nameFilter,
  111.             'sort' =>  $sort'sort_direction' =>  $sortDirection'ctr_period' => $ctrPeriod,
  112.             ) = $this->processListRequestData($requesttrue);
  113.         $profilesCtr $profileCtrService->getCtrByOwner($user$ctrPeriod);
  114.         $profileIdsWaitingAction $bulkQueueService->getProfilesWaitingAction($user);
  115.         $isMasseurCriteria $isMasseurList true : ($this->features->non_masseur_on_account_profile_list() ? false null);
  116.         $profiles $this->getFilteredAndSortedProfiles(
  117.             $user$perPage$sort$sortDirection$placementTypeFilter$nameFilter$profilesCtr$isMasseurCriteria
  118.         );
  119.         $bulkAction $this->createForm(BulkActionForm::class, null, [
  120.             'action' => $this->generateUrl('account.profile_management.bulk_action'),
  121.             'method' => 'post',
  122.             'data' => [
  123.                 'entity_repository' => $profileRepository,
  124.             ],
  125.         ]);
  126.         $templateVariables = [
  127.             'is_masseur_list' => $isMasseurList,
  128.             'profiles' => $profiles,
  129.             'all_profiles_ids' => $this->showDebugData() ? $this->getProfileIdsByFilter($user$placementTypeFilter$nameFilter$isMasseurList) : [],
  130.             'profiles_ctr' => $profilesCtr,
  131.             'ctr_period' => $ctrPeriod,
  132.             'per_page' => $perPage == 99999 ? -$perPage,
  133.             'placement_type_filter' => $placementTypeFilter,
  134.             'name_filter' => $nameFilter,
  135.             'sort' => $sort,
  136.             'sort_direction' => $sortDirection,
  137.             'profiles_waiting_action' => $profileIdsWaitingAction,
  138.             'plan_placement_enabled' => $planPlacementService->isEnabled(),
  139.             'profile_plan_badges' => $planPlacementService->buildBadgesByProfiles($profiles),
  140.         ];
  141.         if($request->isXmlHttpRequest()) {
  142.             return $this->render('account/profiles/profile_list.profiles.html.twig'$templateVariables);
  143.         } else {
  144.             if(null !== ($redirectResponse $this->redirectToRouteWithoutPaginationIfNeeded($profiles)))
  145.                 return $redirectResponse;
  146.             return $this->render('account/profiles/profile_list.html.twig'array_merge($templateVariables, [
  147.                 'profiles_active_count' => $profileRepository->countActiveOfOwner($user$this->features->non_masseur_on_account_profile_list() ? false null),
  148.                 'profiles_all_count' => $profileRepository->countAllOfOwnerNotDeleted($user$this->features->non_masseur_on_account_profile_list() ? false null),
  149.                 'masseurs_active_count' => $profileRepository->countActiveOfOwner($usertrue),
  150.                 'masseurs_all_count' => $profileRepository->countAllOfOwnerNotDeleted($usertrue),
  151.                 'bulk_form' => $bulkAction->createView(),
  152.                 'per_page_variants' => explode(','$parameterBag->get('account.setting.advertiser_profile_list_per_page.variants')),
  153.                 'ctr_periods' => AccountController::AVAILABLE_CTR_PERIODS,
  154.                 'placement_type_filter_variants' => AccountController::AVAILABLE_PROFILE_PLACEMENT_TYPES,
  155.             ]));
  156.         }
  157.     }
  158.     #[Route(path'/place/{profile}/standard'name'account.profile_management.place_standard'methods: ['POST'])]
  159.     #[IsGranted('ROLE_ADVERTISER')]
  160.     public function placeAtStandardPosition(Request $requestProfile $profileProfileAdBoard $profileAdBoard): RedirectResponse
  161.     {
  162.         try {
  163.             $this->assertProfileOwner($profile);
  164.             $csrfToken $request->request->get('_csrf_token');
  165.             if ($this->isCsrfTokenValid('profile-place-'.$profile->getId(), $csrfToken)) {
  166.                 $this->assertProfileIsNotWaitingBulkAction($profile);
  167.                 $this->assertProfileIsEligibleToShow($profile);
  168.                 $profileAdBoard->placeAtStandardPosition($profile);
  169.                 $this->addFlash('success''Анкета размещена на сайте');
  170.             }
  171.         } catch (\Exception $ex) {
  172.             $this->addFlash('error'$ex->getMessage());
  173.         }
  174.         return $this->redirectToList($profile->isMasseur(), true);
  175.     }
  176.     #[Route(path'/place/{profile}/vip'name'account.profile_management.place_vip'methods: ['POST'])]
  177.     #[IsGranted('ROLE_ADVERTISER')]
  178.     public function placeAtVipPosition(Request $requestProfile $profileProfileAdBoard $profileAdBoard): RedirectResponse
  179.     {
  180.         try {
  181.             $this->assertProfileOwner($profile);
  182.             $csrfToken $request->request->get('_csrf_token');
  183.             if ($this->isCsrfTokenValid('profile-place-'.$profile->getId(), $csrfToken)) {
  184.                 $this->assertProfileIsNotWaitingBulkAction($profile);
  185.                 $this->assertProfileIsEligibleToShow($profile);
  186.                 $profileAdBoard->placeAtVipPosition($profile);
  187.                 $this->addFlash('success''Анкета размещена на сайте');
  188.             }
  189.         } catch (\Exception $ex) {
  190.             $this->addFlash('error'$ex->getMessage());
  191.         }
  192.         return $this->redirectToList($profile->isMasseur(), true);
  193.     }
  194.     #[Route(path'/place/{profile}/ultra-vip'name'account.profile_management.place_ultra_vip'methods: ['POST'])]
  195.     #[IsGranted('ROLE_ADVERTISER')]
  196.     public function placeAtUltraVipPosition(Request $requestProfile $profileProfileAdBoard $profileAdBoard): RedirectResponse
  197.     {
  198.         try {
  199.             $this->assertProfileOwner($profile);
  200.             $csrfToken $request->request->get('_csrf_token');
  201.             if ($this->isCsrfTokenValid('profile-place-'.$profile->getId(), $csrfToken)) {
  202.                 $this->assertProfileIsNotWaitingBulkAction($profile);
  203.                 $this->assertProfileIsEligibleToShow($profile);
  204.                 $profileAdBoard->placeAtUltraVipPosition($profile);
  205.                 $this->addFlash('success''Анкета размещена на сайте');
  206.             }
  207.         } catch (\Exception $ex) {
  208.             $this->addFlash('error'$ex->getMessage());
  209.         }
  210.         return $this->redirectToList($profile->isMasseur(), true);
  211.     }
  212.     #[Route(path'/place/{profile}/delete'name'account.profile_management.place_delete'methods: ['POST'])]
  213.     #[IsGranted('ROLE_ADVERTISER')]
  214.     public function deleteFromAdBoard(
  215.         Request $request,
  216.         Profile $profile,
  217.         ProfileAdBoard $profileAdBoard,
  218.         PlacementHider $placementHider,
  219.         ProfilePlanPlacementService $planPlacementService
  220.     ): RedirectResponse
  221.     {
  222.         try {
  223.             $this->assertProfileOwner($profile);
  224.             $csrfToken $request->request->get('_csrf_token');
  225.             if ($this->isCsrfTokenValid('profile-place-'.$profile->getId(), $csrfToken)) {
  226.                 $this->assertProfileIsNotWaitingBulkAction($profile);
  227.                 $this->assertProfileIsEligibleToShow($profile);
  228.                 $profileAdBoard->doRemoveFromAdBoard($profile);
  229.                 $planPlacementService->syncActivePlanPlacementForProfile($profile, new \DateTimeImmutable('now'));
  230. //                if(true === $this->features->restrict_unapproved_profiles_placement_hiding() && false === $profile->isApproved()) {
  231. //                    $placementHider->doHideProfile($profile);
  232. //                }
  233.             }
  234.         } catch (\Exception $ex) {
  235.             $this->addFlash('error'$ex->getMessage());
  236.         }
  237.         return $this->redirectToList($profile->isMasseur());
  238.     }
  239.     #[Route(path'/delete/{profile}'name'account.profile_management.delete'methods: ['POST'])]
  240.     #[IsGranted('ROLE_ADVERTISER')]
  241.     public function deleteProfile(Request $requestProfile $profileEntityManagerInterface $entityManagerProfileRemover $profileRemover): RedirectResponse
  242.     {
  243.         if(false == $this->features->allow_profile_delete()) {
  244.             throw new \LogicException('Deletion of profiles is disabled.');
  245.         }
  246.         try {
  247.             $this->assertProfileOwner($profile);
  248.             $csrfToken $request->request->get('_csrf_token');
  249.             if ($this->isCsrfTokenValid('profile-delete-'.$profile->getId(), $csrfToken)) {
  250.                 $profileRemover->delete($profile);
  251.                 $entityManager->flush();
  252.                 $this->addFlash('success''Анкета удалена');
  253.             }
  254.         } catch (\Exception $ex) {
  255.             $this->addFlash('error'$ex->getMessage());
  256.         }
  257.         return $this->redirectToList($profile->isMasseur());
  258.     }
  259.     #[Route(path'/bulk-action'name'account.profile_management.bulk_action'methods: ['POST'])]
  260.     #[IsGranted('ROLE_ADVERTISER')]
  261.     public function bulkAction(Request $requestProfileRepository $profileRepositoryBulkService $bulkServiceParameterBagInterface $parameterBag): RedirectResponse
  262.     {
  263.         $bulkAction $this->createForm(BulkActionForm::class, null, [
  264.             'data' => [
  265.                 'entity_repository' => $profileRepository,
  266.             ],
  267.         ]);
  268.         $clickedButton null;
  269.         $maxEntitiesToProcessImmediately $parameterBag->get('app.profile.bulk_action.max_entities_to_process_immediately');
  270.         $bulkAction->handleRequest($request);
  271.         if ($bulkAction->isSubmitted() && $bulkAction->isValid()) {
  272.             $profilesIds null;
  273.             $placementTypeFilter = isset($request->get('bulk_action_form')['placement_type_filter']) ? $request->get('bulk_action_form')['placement_type_filter'] : null;
  274.             $nameFilter = isset($request->get('bulk_action_form')['name_filter']) ? $request->get('bulk_action_form')['name_filter'] : -1;
  275.             //-1 для отделения от значения null, которое может быть передано при xhr и означает, что не имеет значение массажистка/нет
  276.             $isMasseur = -1;
  277.             if(isset($request->get('bulk_action_form')['is_masseur'])) {
  278.                 $isMasseurRaw $request->get('bulk_action_form')['is_masseur'];
  279.                 switch ($request->get('bulk_action_form')['is_masseur']) {
  280.                     case ($isMasseurRaw === 'true'): $isMasseur true; break;
  281.                     case ($isMasseurRaw === 'false'): $isMasseur false; break;
  282.                     case ($isMasseurRaw === 'null'): $isMasseur null; break;
  283.                 }
  284.             }
  285.             if(isset($request->get('bulk_action_form')['entity']['all'])) {
  286.                 if(null === $placementTypeFilter || -=== $nameFilter || -=== $isMasseur) {
  287.                     throw new \LogicException('Filters not specified for \'all\' bulk action.');
  288.                 }
  289.                 //$profiles = $profileRepository->ofOwnerAndMasseurTypeWithPlacementFilterAndNameFilterIterateAll($this->getUser(), $placementTypeFilter, $nameFilter, $isMasseur);
  290.                 //$profilesCount = count($profilesIds);
  291.                 $profilesIds $profileRepository->idsOfOwnerAndMasseurTypeWithPlacementFilterAndNameFilter($this->getUser(), $placementTypeFilter$nameFilter$isMasseur);
  292.             } else {
  293.                 //$profiles = $bulkAction->getData();
  294.                 //$profilesIds = array_map(function(Profile $profile) { return $profile->getId(); }, $profiles);
  295.                 //$profilesCount = count($profiles);
  296.                 $profilesIds = isset($request->get('bulk_action_form')['entity']) ? $request->get('bulk_action_form')['entity'] : [];
  297.             }
  298.             if(!$bulkAction->getClickedButton())
  299.                 throw new \LogicException('Bulk action not specified');
  300.             $clickedButton $bulkAction->getClickedButton()->getName();
  301.             $profilesCount count($profilesIds);
  302.             if($profilesCount $maxEntitiesToProcessImmediately) {
  303.                 $this->bulkQueueService->addProfilesBulk($this->getUser(), $clickedButton$profilesIds);
  304.                 $this->addFlash('notification'sprintf('Массовые действия обрабатываются с задержкой если выбрано больше чем %s анкет (выбрано %s)'$maxEntitiesToProcessImmediately$profilesCount));
  305.                 return $this->redirectToList(isset($isMasseur) && $isMasseur != -$isMasseur false'delete' != $clickedButton);
  306.             } else {
  307.                 $processedProfilesMessages $bulkService->profilesBulkAction($this->getUser(), $clickedButton$profilesIds);
  308.                 foreach ($processedProfilesMessages as $message) {
  309.                     $this->addFlash($message['type'], $message['message']);
  310.                 }
  311.             }
  312.             if($this->showDebugData() && $profilesIds && count($profilesIds)) {
  313.                 $this->addFlash('notification',
  314.                     sprintf("Action processed for filters placement: %s, name: %s, masseur: %s profiles: %s",
  315.                         $placementTypeFilter$nameFilter$isMasseurimplode(','$profilesIds)
  316.                     )
  317.                 );
  318.             }
  319.         }
  320.         return $this->redirectToList(isset($isMasseur) && $isMasseur != -$isMasseur false'delete' != $clickedButton);
  321.     }
  322.     private function redirectToList(bool $isMasseurbool $toPage false): RedirectResponse
  323.     {
  324.         $refererRouteName $this->getRefererRouteName($this->request);
  325.         if($refererRouteName == 'account' || $refererRouteName == 'account._pagination')
  326.             $redirectRoute 'account';
  327.         else
  328.             $redirectRoute $isMasseur 'account.profile_management.list_masseur' 'account.profile_management.list';
  329.         $routeParams = [];
  330.         if($toPage) {
  331.             $referer =  $this->request->headers->get('referer');
  332.             preg_match('/page(\d+)/'$referer$matches);
  333.             if(count($matches)) {
  334.                 $redirectRoute .= '._pagination';
  335.                 $routeParams = [
  336.                     'page' => $matches[1],
  337.                 ];
  338.             }
  339.         }
  340.         return $this->redirectToRoute($redirectRoute$routeParams);
  341.     }
  342.     private function assertProfileOwner(Profile $profile): void
  343.     {
  344.         if (!$profile->isOwnedBy($this->getUser())) {
  345.             throw new \DomainException('Анкета не найдена');
  346.         }
  347.     }
  348.     private function assertProfileIsEligibleToShow(Profile $profile): void
  349.     {
  350.         if (false == $this->moderationService->isProfileEligibleToShowByModeration($profile)) {
  351.             throw new \DomainException('Это действие запрещено модерацией.');
  352.         }
  353.     }
  354.     private function assertProfileIsNotWaitingBulkAction(Profile $profile): void
  355.     {
  356.         if (true == $this->bulkQueueService->isProfileWaitingAction($profile)) {
  357.             throw new \DomainException('Для этой анкеты уже запланировано действие.');
  358.         }
  359.     }
  360.     private function assertProfileIsEligibleForPlacementHidingRemoval(Profile $profile): void
  361.     {
  362.         if (true === $this->features->restrict_unapproved_profile_free_placement() && false === $profile->isApproved()) {
  363.             throw new \DomainException('Запрещено бесплатное размещение для непроверенных анкет.');
  364.         }
  365.     }
  366.     private function assertProfileIsEligibleForPlacementHiding(Profile $profile): void
  367.     {
  368.         if (true === $this->features->restrict_unapproved_profile_free_placement() && false === $profile->isApproved()) {
  369.             throw new \DomainException('Запрещено скрытие непроверенных анкет.');
  370.         }
  371.     }
  372.     #[Route(path'/top/select/{profile}'name'account.profile_management.top_placement.select')]
  373.     #[IsGranted('ROLE_ADVERTISER')]
  374.     public function topPlacement(Profile $profileRequest $request): Response
  375.     {
  376.         if($request->isXmlHttpRequest()) {
  377.             return new JsonResponse([
  378.                 'code' => 200,
  379.                 'html' => $this->createScheduleHtml($profile)->getContent(),
  380.             ]);
  381.         } else {
  382.             return $this->createScheduleHtml($profile);
  383.         }
  384.     }
  385.     /**
  386.      * @throws \Exception
  387.      */
  388.     #[Route(path'/top/purchase/{profile}'name'account.profile_management.top_placement.purchase')]
  389.     #[IsGranted('ROLE_ADVERTISER')]
  390.     public function purchaseTopPlacement(Profile $profileRequest $requestProfileTopBoard $topBoardCaptchaService $captchaService): JsonResponse
  391.     {
  392.         $data json_decode($this->request->getContent(), true);
  393.         $result['url'] = $this->generateUrl('account.profile_management.top_placement.select', ['profile' => $profile->getId()]);
  394.         $result['html'] = $this->createScheduleHtml($profile)->getContent();
  395.         if($this->features->top_purchase_captcha()) {
  396.             if(!isset($data['g-recaptcha-response']) || false == $captchaService->validateCaptcha($data['g-recaptcha-response'])) {
  397.                 $result['success'] = false;
  398.                 $this->addFlash('success'sprintf('Ошибка безопасности'));
  399.                 return new JsonResponse($result200);
  400.             }
  401.         }
  402.         if(empty($data['token']) || false == $this->csrfTokenManager->isTokenValid(new CsrfToken('purchase_top_placement'$data['token']))){
  403.             $result['success'] = false;
  404.             $this->addFlash('success'sprintf('Ошибка безопасности'));
  405.             return new JsonResponse($result200);
  406.         }
  407.         if(Genders::FEMALE != $profile->getPersonParameters()->getGender()){
  408.             $result['success'] = false;
  409.             $this->addFlash('success'sprintf('Топ разрешен только для девушек.'));
  410.             return new JsonResponse($result200);
  411.         }
  412.         if(false == $this->moderationService->isProfileEligibleToShowByModeration($profile)){
  413.             $result['success'] = false;
  414.             $this->addFlash('success'sprintf('Это действие запрещено модерацией.'));
  415.             return new JsonResponse($result200);
  416.         }
  417.         if($this->getUser() != $profile->getOwner()) {
  418.             $result['success'] = false;
  419.             $this->addFlash('success'sprintf('У вас нет прав для создания этого размещения'));
  420.             return new JsonResponse($result200);
  421.         }
  422.         $dates $data['dates'];
  423.         $timezone $data['timezone'];
  424.         $serverDateTimeZone = (new \DateTimeImmutable())->getTimezone();
  425.         //на всякий случай, если при декоде изменится порядок. Чтобы ближайшие даты обрабатывались первыми.
  426.         ksort($dates);
  427.         $countPlaced 0;
  428.         $dateString '%s %s:00 %s';
  429.         try {
  430.             foreach ($dates as $date => $periods) {
  431.                 foreach ($periods as $period) {
  432.                     $placedAt \DateTimeImmutable::createFromFormat('Y-m-d H:i P'sprintf($dateString$date$period[0], $timezone));
  433.                     $placedAt $placedAt->setTimezone($serverDateTimeZone);
  434.                     $expiresAt \DateTimeImmutable::createFromFormat('Y-m-d H:i P'sprintf($dateString$date$period[1], $timezone));
  435.                     $expiresAt $expiresAt->setTimezone($serverDateTimeZone);
  436.                     $expiresAt $expiresAt->add(new \DateInterval('PT1H'));
  437.                     $topBoard->doPlaceOnTop($profile$placedAt$expiresAt);
  438.                     $countPlaced++;
  439.                 }
  440.             }
  441.             $result['success'] = true;
  442.             $this->addFlash('success''Успешно размещено');
  443.         } catch(\Exception $e) {
  444.             $result['success'] = false;
  445.             if($e instanceof NotEnoughMoneyException) {
  446.                 $this->addFlash('error''Недостаточно средств на счете. Пополните баланс.');
  447.             } else {
  448.                 $this->addFlash('error'sprintf('Что-то пошло не так. Попробуйте еще раз. (код ошибки: %s)'$e->getCode()));
  449.             }
  450.             if($countPlaced) {
  451.                 $this->addFlash('success'sprintf('%s ваших объявлений было размещено.'$countPlaced));
  452.             }
  453.         }
  454.         $result['html'] = $this->createScheduleHtml($profile$data['showFlashes'])->getContent();
  455.         return new JsonResponse($result200);
  456.     }
  457.     #[Route(path'/plan-placement/select/{profile}'name'account.profile_management.plan_placement.select')]
  458.     #[IsGranted('ROLE_ADVERTISER')]
  459.     public function planPlacement(Profile $profileRequest $requestProfilePlanPlacementService $planPlacementService): Response
  460.     {
  461.         if (!$planPlacementService->isEnabledForProfile($profile)) {
  462.             throw new \LogicException('Плановое размещение недоступно.');
  463.         }
  464.         $this->assertProfileOwner($profile);
  465.         $this->assertProfileIsEligibleToShow($profile);
  466.         if($request->isXmlHttpRequest()) {
  467.             $placementTypeValue $request->request->getInt('placement_type');
  468.             if (=== $placementTypeValue) {
  469.                 $placementTypeValue $request->query->getInt('placement_type');
  470.             }
  471.             try {
  472.                 $html $this->createPlanScheduleHtml(
  473.                     $profile,
  474.                     $planPlacementService,
  475.                     true,
  476.                     $placementTypeValue $placementTypeValue null
  477.                 )->getContent();
  478.             } catch (\LogicException $exception) {
  479.                 $this->addFlash('error'$exception->getMessage());
  480.                 $html $this->createPlanScheduleHtml($profile$planPlacementServicetruenull)->getContent();
  481.             }
  482.             return new JsonResponse([
  483.                 'code' => 200,
  484.                 'html' => $html,
  485.             ]);
  486.         }
  487.         $placementTypeValue $request->query->getInt('placement_type');
  488.         try {
  489.             return $this->createPlanScheduleHtml(
  490.                 $profile,
  491.                 $planPlacementService,
  492.                 true,
  493.                 $placementTypeValue $placementTypeValue null
  494.             );
  495.         } catch (\LogicException $exception) {
  496.             $this->addFlash('error'$exception->getMessage());
  497.             return $this->createPlanScheduleHtml($profile$planPlacementServicetruenull);
  498.         }
  499.     }
  500.     #[Route(path'/plan-placement/purchase/{profile}'name'account.profile_management.plan_placement.purchase')]
  501.     #[IsGranted('ROLE_ADVERTISER')]
  502.     public function purchasePlanPlacement(Profile $profileProfilePlanPlacementService $planPlacementService): JsonResponse
  503.     {
  504.         $data json_decode($this->request->getContent(), true);
  505.         if (!is_array($data)) {
  506.             $data = [];
  507.         }
  508.         $showFlashes $data['showFlashes'] ?? true;
  509.         $requestedPlacementTypeValue = isset($data['placement_type']) ? (int)$data['placement_type'] : null;
  510.         $result = [
  511.             'success' => false,
  512.             'need_top_up' => false,
  513.             'top_up_url' => $this->generateUrl('account.finances.pay'),
  514.         ];
  515.         $renderHtml = function (?int $placementTypeValue) use (
  516.             $profile,
  517.             $planPlacementService,
  518.             $showFlashes
  519.         ): string {
  520.             try {
  521.                 return $this->createPlanScheduleHtml(
  522.                     $profile,
  523.                     $planPlacementService,
  524.                     $showFlashes,
  525.                     (null !== $placementTypeValue && $placementTypeValue 0) ? $placementTypeValue null
  526.                 )->getContent();
  527.             } catch (\LogicException $exception) {
  528.                 $this->addFlash('error'$exception->getMessage());
  529.                 return $this->createPlanScheduleHtml($profile$planPlacementService$showFlashesnull)->getContent();
  530.             }
  531.         };
  532.         if (!$planPlacementService->isEnabledForProfile($profile)) {
  533.             $this->addFlash('error''Плановое размещение недоступно.');
  534.             return new JsonResponse($result200);
  535.         }
  536.         if($this->getUser() != $profile->getOwner()) {
  537.             $this->addFlash('error''У вас нет прав для создания этого размещения');
  538.             return new JsonResponse($result200);
  539.         }
  540.         if(false == $this->moderationService->isProfileEligibleToShowByModeration($profile)){
  541.             $this->addFlash('error''Это действие запрещено модерацией.');
  542.             return new JsonResponse($result200);
  543.         }
  544.         if (empty($data['token']) || false == $this->csrfTokenManager->isTokenValid(new CsrfToken('purchase_plan_placement'$data['token']))) {
  545.             $this->addFlash('error''Ошибка безопасности');
  546.             $result['html'] = $renderHtml($requestedPlacementTypeValue);
  547.             return new JsonResponse($result200);
  548.         }
  549.         try {
  550.             $this->assertProfileIsNotWaitingBulkAction($profile);
  551.             $planPlacementService->purchase(
  552.                 $profile,
  553.                 $data['dates'] ?? [],
  554.                 (null !== $requestedPlacementTypeValue && $requestedPlacementTypeValue 0) ? $requestedPlacementTypeValue null
  555.             );
  556.             $result['success'] = true;
  557.             $this->addFlash('success''Успешно запланировано');
  558.         } catch(\Exception $exception) {
  559.             if($exception instanceof NotEnoughMoneyException) {
  560.                 $result['need_top_up'] = true;
  561.                 $this->addFlash('error''Недостаточно средств на счете. Пополните баланс.');
  562.             } else {
  563.                 $this->addFlash('error'$exception->getMessage() ?: 'Что-то пошло не так. Попробуйте еще раз.');
  564.             }
  565.         }
  566.         $result['html'] = $renderHtml($requestedPlacementTypeValue);
  567.         return new JsonResponse($result200);
  568.     }
  569.     private function createScheduleHtml(Profile $profilebool $showFlashes true): Response
  570.     {
  571.         $timezone $this->timeZoneService->getProfileTimeZone($profile);
  572.         $timezoneCityName $this->timeZoneService->getProfileTimeZoneCityName($profile);
  573.         $dateTimeZone = new \DateTimeZone($timezone);
  574.         $now = new \DateTime('now'$dateTimeZone);
  575.         $scheduleStart = new \DateTimeImmutable('today 00:00:00'$dateTimeZone);
  576.         $scheduleEnd = new \DateTimeImmutable('today 00:00:00'$dateTimeZone);
  577.         $scheduleEnd $scheduleEnd->add(new \DateInterval('P7D'))->sub(new \DateInterval('PT1S'));
  578. //        $now->setTimezone($dateTimeZone);
  579. //        $scheduleStart->setTimezone($dateTimeZone);
  580. //        $scheduleEnd->setTimezone($dateTimeZone);
  581.         $serverDateTimeZone = (new \DateTimeImmutable())->getTimezone();
  582.         $topPlacements $this->profileTopPlacementRepository->getPlacementsByPeriod(
  583.             $profile->getCity(),
  584.             $scheduleStart->setTimezone($serverDateTimeZone),
  585.             $scheduleEnd->setTimezone($serverDateTimeZone)
  586.         );
  587.         $dateFormat 'Y-m-d';
  588.         //сетка часов
  589.         $schedule $this->generateSchedule($dateFormat$scheduleStart$scheduleEnd);
  590.         //ставим прошедшие
  591.         foreach ($schedule as $day => &$dayData) {
  592.             foreach ($dayData['hours'] as $hour => $hourData) {
  593.                 if($now->format('Y-m-d H') >= $day sprintf(' %02d'$hour))
  594.                     $dayData['hours'][$hour]['passed'] = true;
  595.             }
  596.         }
  597.         //заполняем расписание занятыми часами
  598.         foreach ($topPlacements as $placement) {
  599.             /** @var TopPlacement $placement */
  600.             /** @var \DatePeriod $overlap */
  601.             $overlap $this->datesOverlap($placement->getPlacedAt()->setTimezone($dateTimeZone), $placement->getExpiresAt()->setTimezone($dateTimeZone), $scheduleStart$scheduleEnd);
  602.             foreach ($overlap as $hour) {
  603.                 /** @var \DateTimeInterface $hour */
  604.                 $schedule[$hour->format($dateFormat)]['hours'][(int)$hour->format('H')] = [
  605.                     'taken' => true,
  606.                     'my' => $placement->getProfile()->getOwner() == $this->getUser()
  607.                 ];
  608.             }
  609.         }
  610.         $topHourlyCharges $this->profileChargesCalculator->calculateTopPlacementCharges($profile, new \DateTime('now'), new \DateTime('now +1 hour'));
  611.         $scheduleSlotPrices = [];
  612.         foreach ($schedule as $dayKey => $dayData) {
  613.             foreach ($dayData['hours'] as $hour => $hourData) {
  614.                 $placedAt = new \DateTimeImmutable(sprintf('%s %02d:00:00'$dayKey, (int)$hour), $dateTimeZone);
  615.                 $placedUntil $placedAt->add(new \DateInterval('PT1H'));
  616.                 $slotCharges $this->profileChargesCalculator->calculateTopPlacementCharges($profile$placedAt$placedUntil);
  617.                 $scheduleSlotPrices[$dayKey][(string)(int)$hour] = (float)number_format(
  618.                     ((int)$slotCharges->getAmount()) / 100,
  619.                     2,
  620.                     '.',
  621.                     ''
  622.                 );
  623.             }
  624.         }
  625.         return $this->render('account/profile_top/profile_top_placement.html.twig', [
  626.             'schedule' => $schedule,
  627.             'price' => $this->moneyFormatterService->formatMoney($topHourlyCharges$this->request->getLocale(), \NumberFormatter::PATTERN_DECIMAL),
  628.             'currency' => $topHourlyCharges->getCurrency(),
  629.             'timezone' => $timezone,
  630.             'timezoneCityName' => $timezoneCityName,
  631.             'schedule_slot_prices' => $scheduleSlotPrices,
  632.             'purchase_url' => $this->generateUrl('account.profile_management.top_placement.purchase', ['profile' => $profile->getId()]),
  633.             'showFlashes' => $showFlashes,
  634.         ]);
  635.     }
  636.     private function createPlanScheduleHtml(
  637.         Profile $profile,
  638.         ProfilePlanPlacementService $planPlacementService,
  639.         bool $showFlashes true,
  640.         ?int $placementTypeValue null
  641.     ): Response
  642.     {
  643.         $scheduleData $planPlacementService->getScheduleWindow($profile$placementTypeValue);
  644.         return $this->render('account/plan_placement/profile_plan_placement.html.twig', [
  645.             'profile' => $profile,
  646.             'schedule' => $scheduleData['schedule'],
  647.             'currency' => $scheduleData['currency'],
  648.             'timezone' => $scheduleData['timezone'],
  649.             'timezoneCityName' => $scheduleData['timezone_city_name'],
  650.             'purchase_url' => $this->generateUrl('account.profile_management.plan_placement.purchase', ['profile' => $profile->getId()]),
  651.             'select_url' => $this->generateUrl('account.profile_management.plan_placement.select', ['profile' => $profile->getId()]),
  652.             'top_up_url' => $this->generateUrl('account.finances.pay'),
  653.             'placement_status_label' => $planPlacementService->getPlacementTypeLabel($scheduleData['placement_type']),
  654.             'placement_type_options' => $scheduleData['placement_type_options'],
  655.             'selected_placement_type' => $scheduleData['placement_type']->getValue(),
  656.             'planned_placements' => $scheduleData['planned'],
  657.             'showFlashes' => $showFlashes,
  658.         ]);
  659.     }
  660.     private function datesOverlap(
  661.         \DateTimeImmutable $startOne\DateTimeImmutable $endOne\DateTimeImmutable $startTwo\DateTimeImmutable $endTwo
  662.     ): \DatePeriod
  663.     {
  664.         if($startOne <= $endTwo && $endOne >= $startTwo) {
  665.             return new \DatePeriod(max($startTwo,$startOne), new \DateInterval('PT1H'), min($endOne,$endTwo));
  666.         }
  667.         throw new \Exception('Временные промежутки не пересекаются');
  668.     }
  669.     private function generateSchedule(string $keyDateFormat\DateTimeInterface $scheduleStart\DateTimeInterface $scheduleEnd): array
  670.     {
  671.         $schedulePeriodDates iterator_to_array(new \DatePeriod($scheduleStart, new \DateInterval('P1D'), $scheduleEnd));
  672.         $schedulePeriodDatesStrings array_map(function(\DateTimeInterface $date) use ($keyDateFormat): string {
  673.             return $date->format($keyDateFormat);
  674.         }, $schedulePeriodDates);
  675.         $dailyHours array_map(function(): array {
  676.             return [
  677.                 'passed' => false,
  678.                 'taken' => false,
  679.                 'my' => false,
  680.             ];//null;
  681.         }, range(023));
  682.         $schedulePeriodHours array_map(function($date) use ($dailyHours): array {
  683.             return ['hours' => $dailyHours'date' => $date];
  684.         }, $schedulePeriodDates);
  685.         $schedule array_combine($schedulePeriodDatesStrings$schedulePeriodHours);
  686.         return $schedule;
  687.     }
  688.     #[Route(path'/{profile}/hide'name'account.profile_management.hide'methods: ['POST'])]
  689.     #[IsGranted('ROLE_ADVERTISER')]
  690.     public function hidePlacement(Request $requestProfile $profilePlacementHider $placementHiderProfileAdBoard $profileAdBoard): RedirectResponse
  691.     {
  692.         try {
  693.             $this->assertProfileOwner($profile);
  694.             $csrfToken $request->request->get('_csrf_token');
  695.             if ($this->isCsrfTokenValid('profile-hide-'.$profile->getId(), $csrfToken)) {
  696.                 $this->assertProfileIsEligibleForPlacementHiding($profile);
  697.                 $this->assertProfileIsNotWaitingBulkAction($profile);
  698.                 $this->assertProfileIsEligibleToShow($profile);
  699.                 $profileAdBoard->doHideProfile($profile);
  700.                 $this->addFlash('success''Анкета спрятана');
  701.             }
  702.         } catch (\Exception $ex) {
  703.             $this->addFlash('error'$ex->getMessage());
  704.         }
  705.         return $this->redirectToList($profile->isMasseur(), true);
  706.     }
  707.     #[Route(path'/{profile}/unhide'name'account.profile_management.unhide'methods: ['POST'])]
  708.     #[IsGranted('ROLE_ADVERTISER')]
  709.     public function unHidePlacement(Request $requestProfile $profilePlacementHider $placementHiderProfileAdBoard $profileAdBoard): RedirectResponse
  710.     {
  711.         try {
  712.             $this->assertProfileOwner($profile);
  713.             $csrfToken $request->request->get('_csrf_token');
  714.             if ($this->isCsrfTokenValid('profile-unhide-'.$profile->getId(), $csrfToken)) {
  715.                 $this->assertProfileIsEligibleForPlacementHidingRemoval($profile);
  716.                 $this->assertProfileIsNotWaitingBulkAction($profile);
  717.                 $this->assertProfileIsEligibleToShow($profile);
  718.                 $profileAdBoard->doUnHideProfile($profile);
  719.                 $this->addFlash('success''Анкета отображена');
  720.             }
  721.         } catch (\Exception $ex) {
  722.             $this->addFlash('error'$ex->getMessage());
  723.         }
  724.         return $this->redirectToList($profile->isMasseur(), true);
  725.     }
  726.     private function showDebugData(): bool
  727.     {
  728.         return $this->kernel->getEnvironment() == 'dev' || $this->kernel->getEnvironment() == 'review';
  729.     }
  730. }