<?php
/**
* Created by simpson <simpsonwork@gmail.com>
* Date: 2019-08-23
* Time: 17:02
*/
namespace App\Controller\Account;
use AngelGamez\PorpaginasBundle\Controller\PaginationTrait;
use App\Controller\RefererTrait;
use App\Entity\Profile\Genders;
use App\Entity\Profile\Profile;
use App\Entity\Sales\Profile\TopPlacement;
use App\Form\BulkActionForm;
use App\PaymentProcessing\Exception\NotEnoughMoneyException;
use App\Repository\ModerationRequestsRepository;
use App\Repository\ProfileRepository;
use App\Repository\ProfileTopPlacementRepository;
use App\Service\BulkQueueService;
use App\Service\BulkService;
use App\Service\CaptchaService;
use App\Service\Features;
use App\Service\ProfileCtrService;
use App\Service\ModerationService;
use App\Service\MoneyFormatterService;
use App\Service\PlacementHider;
use App\Service\ProfileAdBoard;
use App\Service\ProfileChargesCalculator;
use App\Service\ProfilePlanPlacementService;
use App\Service\ProfileRemover;
use App\Service\ProfileTopBoard;
use App\Service\TimeZoneService;
use Doctrine\Persistence\ManagerRegistry;
use Doctrine\ORM\EntityManagerInterface;
use FOS\ElasticaBundle\Persister\PersisterRegistry;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\IsGranted;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\DependencyInjection\ParameterBag\ParameterBagInterface;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\RedirectResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\RequestStack;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpFoundation\Session\SessionInterface;
use Symfony\Component\HttpKernel\KernelInterface;
use Symfony\Component\Routing\Annotation\Route;
use Symfony\Component\Security\Csrf\CsrfToken;
use Symfony\Component\Security\Csrf\CsrfTokenManagerInterface;
class ProfileManagementController extends AbstractController
{
use RefererTrait;
use PaginationTrait;
use AccountListFilterTrait;
const LEGACY_MASSEUR_LIST_PARAM = 'massagers';
private CsrfTokenManagerInterface $csrfTokenManager;
private ProfileTopPlacementRepository $profileTopPlacementRepository;
private MoneyFormatterService $moneyFormatterService;
private TimeZoneService $timeZoneService;
private ProfileChargesCalculator $profileChargesCalculator;
private ModerationService $moderationService;
private ModerationRequestsRepository $moderationRequestsRepository;
private KernelInterface $kernel;
private BulkQueueService $bulkQueueService;
private Features $features;
function __construct(
CsrfTokenManagerInterface $csrfTokenManager, ProfileTopPlacementRepository $profileTopPlacementRepository,
MoneyFormatterService $moneyFormatterService,
RequestStack $requestStack, TimeZoneService $timeZoneService, ProfileChargesCalculator $profileChargesCalculator,
Features $features, ModerationService $moderationService, ModerationRequestsRepository $moderationRequestsRepository,
SessionInterface $session, ParameterBagInterface $parameterBag, KernelInterface $kernel, BulkQueueService $bulkQueueService,
ProfileRepository $profileRepository
)
{
$this->csrfTokenManager = $csrfTokenManager;
$this->profileTopPlacementRepository = $profileTopPlacementRepository;
$this->moneyFormatterService = $moneyFormatterService;
$this->request = $requestStack->getCurrentRequest();
$this->timeZoneService = $timeZoneService;
$this->profileChargesCalculator = $profileChargesCalculator;
$this->moderationService = $moderationService;
$this->moderationRequestsRepository = $moderationRequestsRepository;
$this->session = $session;
$this->parameterBag = $parameterBag;
$this->availablePlacementTypeFilters = AccountController::AVAILABLE_PROFILE_PLACEMENT_TYPES;
$this->kernel = $kernel;
$this->bulkQueueService = $bulkQueueService;
$this->profileRepository = $profileRepository;
$this->features = $features;
}
/**
* @throws \Doctrine\ORM\NoResultException
* @throws \Doctrine\ORM\NonUniqueResultException
*/
#[Route(path: '/', name: 'account_girls')]
#[Route(path: '/page{page<\d+>}/', name: 'account_girls._pagination')]
#[Route(path: '/', name: 'account.profile_management.list')]
#[Route(path: '/page{page<\d+>}/', name: 'account.profile_management.list._pagination', options: ['expose' => true])]
#[Route(path: '/masseur/', name: 'account.profile_management.list_masseur')]
#[Route(path: '/masseur/page{page<\d+>}/', name: 'account.profile_management.list_masseur._pagination', options: ['expose' => true])]
#[IsGranted('ROLE_ADVERTISER')]
public function list(
Request $request, ProfileRepository $profileRepository, ParameterBagInterface $parameterBag,
ProfileCtrService $profileCtrService, BulkQueueService $bulkQueueService, ProfilePlanPlacementService $planPlacementService
): Response
{
$user = $this->getUser();
$isMasseurList = $request->query->has(self::LEGACY_MASSEUR_LIST_PARAM)
|| 'account.profile_management.list_masseur' === $request->attributes->get('_route')
|| 'account.profile_management.list_masseur._pagination' === $request->attributes->get('_route');
list(
'per_page' => $perPage, 'placement_type_filter' => $placementTypeFilter, 'name_filter' => $nameFilter,
'sort' => $sort, 'sort_direction' => $sortDirection, 'ctr_period' => $ctrPeriod,
) = $this->processListRequestData($request, true);
$profilesCtr = $profileCtrService->getCtrByOwner($user, $ctrPeriod);
$profileIdsWaitingAction = $bulkQueueService->getProfilesWaitingAction($user);
$isMasseurCriteria = $isMasseurList ? true : ($this->features->non_masseur_on_account_profile_list() ? false : null);
$profiles = $this->getFilteredAndSortedProfiles(
$user, $perPage, $sort, $sortDirection, $placementTypeFilter, $nameFilter, $profilesCtr, $isMasseurCriteria
);
$bulkAction = $this->createForm(BulkActionForm::class, null, [
'action' => $this->generateUrl('account.profile_management.bulk_action'),
'method' => 'post',
'data' => [
'entity_repository' => $profileRepository,
],
]);
$templateVariables = [
'is_masseur_list' => $isMasseurList,
'profiles' => $profiles,
'all_profiles_ids' => $this->showDebugData() ? $this->getProfileIdsByFilter($user, $placementTypeFilter, $nameFilter, $isMasseurList) : [],
'profiles_ctr' => $profilesCtr,
'ctr_period' => $ctrPeriod,
'per_page' => $perPage == 99999 ? -1 : $perPage,
'placement_type_filter' => $placementTypeFilter,
'name_filter' => $nameFilter,
'sort' => $sort,
'sort_direction' => $sortDirection,
'profiles_waiting_action' => $profileIdsWaitingAction,
'plan_placement_enabled' => $planPlacementService->isEnabled(),
'profile_plan_badges' => $planPlacementService->buildBadgesByProfiles($profiles),
];
if($request->isXmlHttpRequest()) {
return $this->render('account/profiles/profile_list.profiles.html.twig', $templateVariables);
} else {
if(null !== ($redirectResponse = $this->redirectToRouteWithoutPaginationIfNeeded($profiles)))
return $redirectResponse;
return $this->render('account/profiles/profile_list.html.twig', array_merge($templateVariables, [
'profiles_active_count' => $profileRepository->countActiveOfOwner($user, $this->features->non_masseur_on_account_profile_list() ? false : null),
'profiles_all_count' => $profileRepository->countAllOfOwnerNotDeleted($user, $this->features->non_masseur_on_account_profile_list() ? false : null),
'masseurs_active_count' => $profileRepository->countActiveOfOwner($user, true),
'masseurs_all_count' => $profileRepository->countAllOfOwnerNotDeleted($user, true),
'bulk_form' => $bulkAction->createView(),
'per_page_variants' => explode(',', $parameterBag->get('account.setting.advertiser_profile_list_per_page.variants')),
'ctr_periods' => AccountController::AVAILABLE_CTR_PERIODS,
'placement_type_filter_variants' => AccountController::AVAILABLE_PROFILE_PLACEMENT_TYPES,
]));
}
}
#[Route(path: '/place/{profile}/standard', name: 'account.profile_management.place_standard', methods: ['POST'])]
#[IsGranted('ROLE_ADVERTISER')]
public function placeAtStandardPosition(Request $request, Profile $profile, ProfileAdBoard $profileAdBoard): RedirectResponse
{
try {
$this->assertProfileOwner($profile);
$csrfToken = $request->request->get('_csrf_token');
if ($this->isCsrfTokenValid('profile-place-'.$profile->getId(), $csrfToken)) {
$this->assertProfileIsNotWaitingBulkAction($profile);
$this->assertProfileIsEligibleToShow($profile);
$profileAdBoard->placeAtStandardPosition($profile);
$this->addFlash('success', 'Анкета размещена на сайте');
}
} catch (\Exception $ex) {
$this->addFlash('error', $ex->getMessage());
}
return $this->redirectToList($profile->isMasseur(), true);
}
#[Route(path: '/place/{profile}/vip', name: 'account.profile_management.place_vip', methods: ['POST'])]
#[IsGranted('ROLE_ADVERTISER')]
public function placeAtVipPosition(Request $request, Profile $profile, ProfileAdBoard $profileAdBoard): RedirectResponse
{
try {
$this->assertProfileOwner($profile);
$csrfToken = $request->request->get('_csrf_token');
if ($this->isCsrfTokenValid('profile-place-'.$profile->getId(), $csrfToken)) {
$this->assertProfileIsNotWaitingBulkAction($profile);
$this->assertProfileIsEligibleToShow($profile);
$profileAdBoard->placeAtVipPosition($profile);
$this->addFlash('success', 'Анкета размещена на сайте');
}
} catch (\Exception $ex) {
$this->addFlash('error', $ex->getMessage());
}
return $this->redirectToList($profile->isMasseur(), true);
}
#[Route(path: '/place/{profile}/ultra-vip', name: 'account.profile_management.place_ultra_vip', methods: ['POST'])]
#[IsGranted('ROLE_ADVERTISER')]
public function placeAtUltraVipPosition(Request $request, Profile $profile, ProfileAdBoard $profileAdBoard): RedirectResponse
{
try {
$this->assertProfileOwner($profile);
$csrfToken = $request->request->get('_csrf_token');
if ($this->isCsrfTokenValid('profile-place-'.$profile->getId(), $csrfToken)) {
$this->assertProfileIsNotWaitingBulkAction($profile);
$this->assertProfileIsEligibleToShow($profile);
$profileAdBoard->placeAtUltraVipPosition($profile);
$this->addFlash('success', 'Анкета размещена на сайте');
}
} catch (\Exception $ex) {
$this->addFlash('error', $ex->getMessage());
}
return $this->redirectToList($profile->isMasseur(), true);
}
#[Route(path: '/place/{profile}/delete', name: 'account.profile_management.place_delete', methods: ['POST'])]
#[IsGranted('ROLE_ADVERTISER')]
public function deleteFromAdBoard(
Request $request,
Profile $profile,
ProfileAdBoard $profileAdBoard,
PlacementHider $placementHider,
ProfilePlanPlacementService $planPlacementService
): RedirectResponse
{
try {
$this->assertProfileOwner($profile);
$csrfToken = $request->request->get('_csrf_token');
if ($this->isCsrfTokenValid('profile-place-'.$profile->getId(), $csrfToken)) {
$this->assertProfileIsNotWaitingBulkAction($profile);
$this->assertProfileIsEligibleToShow($profile);
$profileAdBoard->doRemoveFromAdBoard($profile);
$planPlacementService->syncActivePlanPlacementForProfile($profile, new \DateTimeImmutable('now'));
// if(true === $this->features->restrict_unapproved_profiles_placement_hiding() && false === $profile->isApproved()) {
// $placementHider->doHideProfile($profile);
// }
}
} catch (\Exception $ex) {
$this->addFlash('error', $ex->getMessage());
}
return $this->redirectToList($profile->isMasseur());
}
#[Route(path: '/delete/{profile}', name: 'account.profile_management.delete', methods: ['POST'])]
#[IsGranted('ROLE_ADVERTISER')]
public function deleteProfile(Request $request, Profile $profile, EntityManagerInterface $entityManager, ProfileRemover $profileRemover): RedirectResponse
{
if(false == $this->features->allow_profile_delete()) {
throw new \LogicException('Deletion of profiles is disabled.');
}
try {
$this->assertProfileOwner($profile);
$csrfToken = $request->request->get('_csrf_token');
if ($this->isCsrfTokenValid('profile-delete-'.$profile->getId(), $csrfToken)) {
$profileRemover->delete($profile);
$entityManager->flush();
$this->addFlash('success', 'Анкета удалена');
}
} catch (\Exception $ex) {
$this->addFlash('error', $ex->getMessage());
}
return $this->redirectToList($profile->isMasseur());
}
#[Route(path: '/bulk-action', name: 'account.profile_management.bulk_action', methods: ['POST'])]
#[IsGranted('ROLE_ADVERTISER')]
public function bulkAction(Request $request, ProfileRepository $profileRepository, BulkService $bulkService, ParameterBagInterface $parameterBag): RedirectResponse
{
$bulkAction = $this->createForm(BulkActionForm::class, null, [
'data' => [
'entity_repository' => $profileRepository,
],
]);
$clickedButton = null;
$maxEntitiesToProcessImmediately = $parameterBag->get('app.profile.bulk_action.max_entities_to_process_immediately');
$bulkAction->handleRequest($request);
if ($bulkAction->isSubmitted() && $bulkAction->isValid()) {
$profilesIds = null;
$placementTypeFilter = isset($request->get('bulk_action_form')['placement_type_filter']) ? $request->get('bulk_action_form')['placement_type_filter'] : null;
$nameFilter = isset($request->get('bulk_action_form')['name_filter']) ? $request->get('bulk_action_form')['name_filter'] : -1;
//-1 для отделения от значения null, которое может быть передано при xhr и означает, что не имеет значение массажистка/нет
$isMasseur = -1;
if(isset($request->get('bulk_action_form')['is_masseur'])) {
$isMasseurRaw = $request->get('bulk_action_form')['is_masseur'];
switch ($request->get('bulk_action_form')['is_masseur']) {
case ($isMasseurRaw === 'true'): $isMasseur = true; break;
case ($isMasseurRaw === 'false'): $isMasseur = false; break;
case ($isMasseurRaw === 'null'): $isMasseur = null; break;
}
}
if(isset($request->get('bulk_action_form')['entity']['all'])) {
if(null === $placementTypeFilter || -1 === $nameFilter || -1 === $isMasseur) {
throw new \LogicException('Filters not specified for \'all\' bulk action.');
}
//$profiles = $profileRepository->ofOwnerAndMasseurTypeWithPlacementFilterAndNameFilterIterateAll($this->getUser(), $placementTypeFilter, $nameFilter, $isMasseur);
//$profilesCount = count($profilesIds);
$profilesIds = $profileRepository->idsOfOwnerAndMasseurTypeWithPlacementFilterAndNameFilter($this->getUser(), $placementTypeFilter, $nameFilter, $isMasseur);
} else {
//$profiles = $bulkAction->getData();
//$profilesIds = array_map(function(Profile $profile) { return $profile->getId(); }, $profiles);
//$profilesCount = count($profiles);
$profilesIds = isset($request->get('bulk_action_form')['entity']) ? $request->get('bulk_action_form')['entity'] : [];
}
if(!$bulkAction->getClickedButton())
throw new \LogicException('Bulk action not specified');
$clickedButton = $bulkAction->getClickedButton()->getName();
$profilesCount = count($profilesIds);
if($profilesCount > $maxEntitiesToProcessImmediately) {
$this->bulkQueueService->addProfilesBulk($this->getUser(), $clickedButton, $profilesIds);
$this->addFlash('notification', sprintf('Массовые действия обрабатываются с задержкой если выбрано больше чем %s анкет (выбрано %s)', $maxEntitiesToProcessImmediately, $profilesCount));
return $this->redirectToList(isset($isMasseur) && $isMasseur != -1 ? $isMasseur : false, 'delete' != $clickedButton);
} else {
$processedProfilesMessages = $bulkService->profilesBulkAction($this->getUser(), $clickedButton, $profilesIds);
foreach ($processedProfilesMessages as $message) {
$this->addFlash($message['type'], $message['message']);
}
}
if($this->showDebugData() && $profilesIds && count($profilesIds)) {
$this->addFlash('notification',
sprintf("Action processed for filters placement: %s, name: %s, masseur: %s profiles: %s",
$placementTypeFilter, $nameFilter, $isMasseur, implode(',', $profilesIds)
)
);
}
}
return $this->redirectToList(isset($isMasseur) && $isMasseur != -1 ? $isMasseur : false, 'delete' != $clickedButton);
}
private function redirectToList(bool $isMasseur, bool $toPage = false): RedirectResponse
{
$refererRouteName = $this->getRefererRouteName($this->request);
if($refererRouteName == 'account' || $refererRouteName == 'account._pagination')
$redirectRoute = 'account';
else
$redirectRoute = $isMasseur ? 'account.profile_management.list_masseur' : 'account.profile_management.list';
$routeParams = [];
if($toPage) {
$referer = $this->request->headers->get('referer');
preg_match('/page(\d+)/', $referer, $matches);
if(count($matches)) {
$redirectRoute .= '._pagination';
$routeParams = [
'page' => $matches[1],
];
}
}
return $this->redirectToRoute($redirectRoute, $routeParams);
}
private function assertProfileOwner(Profile $profile): void
{
if (!$profile->isOwnedBy($this->getUser())) {
throw new \DomainException('Анкета не найдена');
}
}
private function assertProfileIsEligibleToShow(Profile $profile): void
{
if (false == $this->moderationService->isProfileEligibleToShowByModeration($profile)) {
throw new \DomainException('Это действие запрещено модерацией.');
}
}
private function assertProfileIsNotWaitingBulkAction(Profile $profile): void
{
if (true == $this->bulkQueueService->isProfileWaitingAction($profile)) {
throw new \DomainException('Для этой анкеты уже запланировано действие.');
}
}
private function assertProfileIsEligibleForPlacementHidingRemoval(Profile $profile): void
{
if (true === $this->features->restrict_unapproved_profile_free_placement() && false === $profile->isApproved()) {
throw new \DomainException('Запрещено бесплатное размещение для непроверенных анкет.');
}
}
private function assertProfileIsEligibleForPlacementHiding(Profile $profile): void
{
if (true === $this->features->restrict_unapproved_profile_free_placement() && false === $profile->isApproved()) {
throw new \DomainException('Запрещено скрытие непроверенных анкет.');
}
}
#[Route(path: '/top/select/{profile}', name: 'account.profile_management.top_placement.select')]
#[IsGranted('ROLE_ADVERTISER')]
public function topPlacement(Profile $profile, Request $request): Response
{
if($request->isXmlHttpRequest()) {
return new JsonResponse([
'code' => 200,
'html' => $this->createScheduleHtml($profile)->getContent(),
]);
} else {
return $this->createScheduleHtml($profile);
}
}
/**
* @throws \Exception
*/
#[Route(path: '/top/purchase/{profile}', name: 'account.profile_management.top_placement.purchase')]
#[IsGranted('ROLE_ADVERTISER')]
public function purchaseTopPlacement(Profile $profile, Request $request, ProfileTopBoard $topBoard, CaptchaService $captchaService): JsonResponse
{
$data = json_decode($this->request->getContent(), true);
$result['url'] = $this->generateUrl('account.profile_management.top_placement.select', ['profile' => $profile->getId()]);
$result['html'] = $this->createScheduleHtml($profile)->getContent();
if($this->features->top_purchase_captcha()) {
if(!isset($data['g-recaptcha-response']) || false == $captchaService->validateCaptcha($data['g-recaptcha-response'])) {
$result['success'] = false;
$this->addFlash('success', sprintf('Ошибка безопасности'));
return new JsonResponse($result, 200);
}
}
if(empty($data['token']) || false == $this->csrfTokenManager->isTokenValid(new CsrfToken('purchase_top_placement', $data['token']))){
$result['success'] = false;
$this->addFlash('success', sprintf('Ошибка безопасности'));
return new JsonResponse($result, 200);
}
if(Genders::FEMALE != $profile->getPersonParameters()->getGender()){
$result['success'] = false;
$this->addFlash('success', sprintf('Топ разрешен только для девушек.'));
return new JsonResponse($result, 200);
}
if(false == $this->moderationService->isProfileEligibleToShowByModeration($profile)){
$result['success'] = false;
$this->addFlash('success', sprintf('Это действие запрещено модерацией.'));
return new JsonResponse($result, 200);
}
if($this->getUser() != $profile->getOwner()) {
$result['success'] = false;
$this->addFlash('success', sprintf('У вас нет прав для создания этого размещения'));
return new JsonResponse($result, 200);
}
$dates = $data['dates'];
$timezone = $data['timezone'];
$serverDateTimeZone = (new \DateTimeImmutable())->getTimezone();
//на всякий случай, если при декоде изменится порядок. Чтобы ближайшие даты обрабатывались первыми.
ksort($dates);
$countPlaced = 0;
$dateString = '%s %s:00 %s';
try {
foreach ($dates as $date => $periods) {
foreach ($periods as $period) {
$placedAt = \DateTimeImmutable::createFromFormat('Y-m-d H:i P', sprintf($dateString, $date, $period[0], $timezone));
$placedAt = $placedAt->setTimezone($serverDateTimeZone);
$expiresAt = \DateTimeImmutable::createFromFormat('Y-m-d H:i P', sprintf($dateString, $date, $period[1], $timezone));
$expiresAt = $expiresAt->setTimezone($serverDateTimeZone);
$expiresAt = $expiresAt->add(new \DateInterval('PT1H'));
$topBoard->doPlaceOnTop($profile, $placedAt, $expiresAt);
$countPlaced++;
}
}
$result['success'] = true;
$this->addFlash('success', 'Успешно размещено');
} catch(\Exception $e) {
$result['success'] = false;
if($e instanceof NotEnoughMoneyException) {
$this->addFlash('error', 'Недостаточно средств на счете. Пополните баланс.');
} else {
$this->addFlash('error', sprintf('Что-то пошло не так. Попробуйте еще раз. (код ошибки: %s)', $e->getCode()));
}
if($countPlaced) {
$this->addFlash('success', sprintf('%s ваших объявлений было размещено.', $countPlaced));
}
}
$result['html'] = $this->createScheduleHtml($profile, $data['showFlashes'])->getContent();
return new JsonResponse($result, 200);
}
#[Route(path: '/plan-placement/select/{profile}', name: 'account.profile_management.plan_placement.select')]
#[IsGranted('ROLE_ADVERTISER')]
public function planPlacement(Profile $profile, Request $request, ProfilePlanPlacementService $planPlacementService): Response
{
if (!$planPlacementService->isEnabledForProfile($profile)) {
throw new \LogicException('Плановое размещение недоступно.');
}
$this->assertProfileOwner($profile);
$this->assertProfileIsEligibleToShow($profile);
if($request->isXmlHttpRequest()) {
$placementTypeValue = $request->request->getInt('placement_type');
if (0 === $placementTypeValue) {
$placementTypeValue = $request->query->getInt('placement_type');
}
try {
$html = $this->createPlanScheduleHtml(
$profile,
$planPlacementService,
true,
$placementTypeValue > 0 ? $placementTypeValue : null
)->getContent();
} catch (\LogicException $exception) {
$this->addFlash('error', $exception->getMessage());
$html = $this->createPlanScheduleHtml($profile, $planPlacementService, true, null)->getContent();
}
return new JsonResponse([
'code' => 200,
'html' => $html,
]);
}
$placementTypeValue = $request->query->getInt('placement_type');
try {
return $this->createPlanScheduleHtml(
$profile,
$planPlacementService,
true,
$placementTypeValue > 0 ? $placementTypeValue : null
);
} catch (\LogicException $exception) {
$this->addFlash('error', $exception->getMessage());
return $this->createPlanScheduleHtml($profile, $planPlacementService, true, null);
}
}
#[Route(path: '/plan-placement/purchase/{profile}', name: 'account.profile_management.plan_placement.purchase')]
#[IsGranted('ROLE_ADVERTISER')]
public function purchasePlanPlacement(Profile $profile, ProfilePlanPlacementService $planPlacementService): JsonResponse
{
$data = json_decode($this->request->getContent(), true);
if (!is_array($data)) {
$data = [];
}
$showFlashes = $data['showFlashes'] ?? true;
$requestedPlacementTypeValue = isset($data['placement_type']) ? (int)$data['placement_type'] : null;
$result = [
'success' => false,
'need_top_up' => false,
'top_up_url' => $this->generateUrl('account.finances.pay'),
];
$renderHtml = function (?int $placementTypeValue) use (
$profile,
$planPlacementService,
$showFlashes
): string {
try {
return $this->createPlanScheduleHtml(
$profile,
$planPlacementService,
$showFlashes,
(null !== $placementTypeValue && $placementTypeValue > 0) ? $placementTypeValue : null
)->getContent();
} catch (\LogicException $exception) {
$this->addFlash('error', $exception->getMessage());
return $this->createPlanScheduleHtml($profile, $planPlacementService, $showFlashes, null)->getContent();
}
};
if (!$planPlacementService->isEnabledForProfile($profile)) {
$this->addFlash('error', 'Плановое размещение недоступно.');
return new JsonResponse($result, 200);
}
if($this->getUser() != $profile->getOwner()) {
$this->addFlash('error', 'У вас нет прав для создания этого размещения');
return new JsonResponse($result, 200);
}
if(false == $this->moderationService->isProfileEligibleToShowByModeration($profile)){
$this->addFlash('error', 'Это действие запрещено модерацией.');
return new JsonResponse($result, 200);
}
if (empty($data['token']) || false == $this->csrfTokenManager->isTokenValid(new CsrfToken('purchase_plan_placement', $data['token']))) {
$this->addFlash('error', 'Ошибка безопасности');
$result['html'] = $renderHtml($requestedPlacementTypeValue);
return new JsonResponse($result, 200);
}
try {
$this->assertProfileIsNotWaitingBulkAction($profile);
$planPlacementService->purchase(
$profile,
$data['dates'] ?? [],
(null !== $requestedPlacementTypeValue && $requestedPlacementTypeValue > 0) ? $requestedPlacementTypeValue : null
);
$result['success'] = true;
$this->addFlash('success', 'Успешно запланировано');
} catch(\Exception $exception) {
if($exception instanceof NotEnoughMoneyException) {
$result['need_top_up'] = true;
$this->addFlash('error', 'Недостаточно средств на счете. Пополните баланс.');
} else {
$this->addFlash('error', $exception->getMessage() ?: 'Что-то пошло не так. Попробуйте еще раз.');
}
}
$result['html'] = $renderHtml($requestedPlacementTypeValue);
return new JsonResponse($result, 200);
}
private function createScheduleHtml(Profile $profile, bool $showFlashes = true): Response
{
$timezone = $this->timeZoneService->getProfileTimeZone($profile);
$timezoneCityName = $this->timeZoneService->getProfileTimeZoneCityName($profile);
$dateTimeZone = new \DateTimeZone($timezone);
$now = new \DateTime('now', $dateTimeZone);
$scheduleStart = new \DateTimeImmutable('today 00:00:00', $dateTimeZone);
$scheduleEnd = new \DateTimeImmutable('today 00:00:00', $dateTimeZone);
$scheduleEnd = $scheduleEnd->add(new \DateInterval('P7D'))->sub(new \DateInterval('PT1S'));
// $now->setTimezone($dateTimeZone);
// $scheduleStart->setTimezone($dateTimeZone);
// $scheduleEnd->setTimezone($dateTimeZone);
$serverDateTimeZone = (new \DateTimeImmutable())->getTimezone();
$topPlacements = $this->profileTopPlacementRepository->getPlacementsByPeriod(
$profile->getCity(),
$scheduleStart->setTimezone($serverDateTimeZone),
$scheduleEnd->setTimezone($serverDateTimeZone)
);
$dateFormat = 'Y-m-d';
//сетка часов
$schedule = $this->generateSchedule($dateFormat, $scheduleStart, $scheduleEnd);
//ставим прошедшие
foreach ($schedule as $day => &$dayData) {
foreach ($dayData['hours'] as $hour => $hourData) {
if($now->format('Y-m-d H') >= $day . sprintf(' %02d', $hour))
$dayData['hours'][$hour]['passed'] = true;
}
}
//заполняем расписание занятыми часами
foreach ($topPlacements as $placement) {
/** @var TopPlacement $placement */
/** @var \DatePeriod $overlap */
$overlap = $this->datesOverlap($placement->getPlacedAt()->setTimezone($dateTimeZone), $placement->getExpiresAt()->setTimezone($dateTimeZone), $scheduleStart, $scheduleEnd);
foreach ($overlap as $hour) {
/** @var \DateTimeInterface $hour */
$schedule[$hour->format($dateFormat)]['hours'][(int)$hour->format('H')] = [
'taken' => true,
'my' => $placement->getProfile()->getOwner() == $this->getUser()
];
}
}
$topHourlyCharges = $this->profileChargesCalculator->calculateTopPlacementCharges($profile, new \DateTime('now'), new \DateTime('now +1 hour'));
$scheduleSlotPrices = [];
foreach ($schedule as $dayKey => $dayData) {
foreach ($dayData['hours'] as $hour => $hourData) {
$placedAt = new \DateTimeImmutable(sprintf('%s %02d:00:00', $dayKey, (int)$hour), $dateTimeZone);
$placedUntil = $placedAt->add(new \DateInterval('PT1H'));
$slotCharges = $this->profileChargesCalculator->calculateTopPlacementCharges($profile, $placedAt, $placedUntil);
$scheduleSlotPrices[$dayKey][(string)(int)$hour] = (float)number_format(
((int)$slotCharges->getAmount()) / 100,
2,
'.',
''
);
}
}
return $this->render('account/profile_top/profile_top_placement.html.twig', [
'schedule' => $schedule,
'price' => $this->moneyFormatterService->formatMoney($topHourlyCharges, $this->request->getLocale(), \NumberFormatter::PATTERN_DECIMAL),
'currency' => $topHourlyCharges->getCurrency(),
'timezone' => $timezone,
'timezoneCityName' => $timezoneCityName,
'schedule_slot_prices' => $scheduleSlotPrices,
'purchase_url' => $this->generateUrl('account.profile_management.top_placement.purchase', ['profile' => $profile->getId()]),
'showFlashes' => $showFlashes,
]);
}
private function createPlanScheduleHtml(
Profile $profile,
ProfilePlanPlacementService $planPlacementService,
bool $showFlashes = true,
?int $placementTypeValue = null
): Response
{
$scheduleData = $planPlacementService->getScheduleWindow($profile, $placementTypeValue);
return $this->render('account/plan_placement/profile_plan_placement.html.twig', [
'profile' => $profile,
'schedule' => $scheduleData['schedule'],
'currency' => $scheduleData['currency'],
'timezone' => $scheduleData['timezone'],
'timezoneCityName' => $scheduleData['timezone_city_name'],
'purchase_url' => $this->generateUrl('account.profile_management.plan_placement.purchase', ['profile' => $profile->getId()]),
'select_url' => $this->generateUrl('account.profile_management.plan_placement.select', ['profile' => $profile->getId()]),
'top_up_url' => $this->generateUrl('account.finances.pay'),
'placement_status_label' => $planPlacementService->getPlacementTypeLabel($scheduleData['placement_type']),
'placement_type_options' => $scheduleData['placement_type_options'],
'selected_placement_type' => $scheduleData['placement_type']->getValue(),
'planned_placements' => $scheduleData['planned'],
'showFlashes' => $showFlashes,
]);
}
private function datesOverlap(
\DateTimeImmutable $startOne, \DateTimeImmutable $endOne, \DateTimeImmutable $startTwo, \DateTimeImmutable $endTwo
): \DatePeriod
{
if($startOne <= $endTwo && $endOne >= $startTwo) {
return new \DatePeriod(max($startTwo,$startOne), new \DateInterval('PT1H'), min($endOne,$endTwo));
}
throw new \Exception('Временные промежутки не пересекаются');
}
private function generateSchedule(string $keyDateFormat, \DateTimeInterface $scheduleStart, \DateTimeInterface $scheduleEnd): array
{
$schedulePeriodDates = iterator_to_array(new \DatePeriod($scheduleStart, new \DateInterval('P1D'), $scheduleEnd));
$schedulePeriodDatesStrings = array_map(function(\DateTimeInterface $date) use ($keyDateFormat): string {
return $date->format($keyDateFormat);
}, $schedulePeriodDates);
$dailyHours = array_map(function(): array {
return [
'passed' => false,
'taken' => false,
'my' => false,
];//null;
}, range(0, 23));
$schedulePeriodHours = array_map(function($date) use ($dailyHours): array {
return ['hours' => $dailyHours, 'date' => $date];
}, $schedulePeriodDates);
$schedule = array_combine($schedulePeriodDatesStrings, $schedulePeriodHours);
return $schedule;
}
#[Route(path: '/{profile}/hide', name: 'account.profile_management.hide', methods: ['POST'])]
#[IsGranted('ROLE_ADVERTISER')]
public function hidePlacement(Request $request, Profile $profile, PlacementHider $placementHider, ProfileAdBoard $profileAdBoard): RedirectResponse
{
try {
$this->assertProfileOwner($profile);
$csrfToken = $request->request->get('_csrf_token');
if ($this->isCsrfTokenValid('profile-hide-'.$profile->getId(), $csrfToken)) {
$this->assertProfileIsEligibleForPlacementHiding($profile);
$this->assertProfileIsNotWaitingBulkAction($profile);
$this->assertProfileIsEligibleToShow($profile);
$profileAdBoard->doHideProfile($profile);
$this->addFlash('success', 'Анкета спрятана');
}
} catch (\Exception $ex) {
$this->addFlash('error', $ex->getMessage());
}
return $this->redirectToList($profile->isMasseur(), true);
}
#[Route(path: '/{profile}/unhide', name: 'account.profile_management.unhide', methods: ['POST'])]
#[IsGranted('ROLE_ADVERTISER')]
public function unHidePlacement(Request $request, Profile $profile, PlacementHider $placementHider, ProfileAdBoard $profileAdBoard): RedirectResponse
{
try {
$this->assertProfileOwner($profile);
$csrfToken = $request->request->get('_csrf_token');
if ($this->isCsrfTokenValid('profile-unhide-'.$profile->getId(), $csrfToken)) {
$this->assertProfileIsEligibleForPlacementHidingRemoval($profile);
$this->assertProfileIsNotWaitingBulkAction($profile);
$this->assertProfileIsEligibleToShow($profile);
$profileAdBoard->doUnHideProfile($profile);
$this->addFlash('success', 'Анкета отображена');
}
} catch (\Exception $ex) {
$this->addFlash('error', $ex->getMessage());
}
return $this->redirectToList($profile->isMasseur(), true);
}
private function showDebugData(): bool
{
return $this->kernel->getEnvironment() == 'dev' || $this->kernel->getEnvironment() == 'review';
}
}