src/Entity/Profile/Profile.php line 77

Open in your IDE?
  1. <?php
  2. /**
  3.  * Created by simpson <simpsonwork@gmail.com>
  4.  * Date: 2019-03-19
  5.  * Time: 15:36
  6.  */
  7. namespace App\Entity\Profile;
  8. use AngelGamez\TranslatableBundle\Entity\TranslatableValue;
  9. //use ApiPlatform\Core\Annotation\ApiProperty;
  10. use App\Entity\Account\Advertiser;
  11. use App\Entity\ApartmentsPricing;
  12. use App\Entity\ContainsDomainEvents;
  13. use App\Entity\Account\Customer;
  14. use App\Entity\DomainEventsRecorderTrait;
  15. use App\Entity\ExpressPricing;
  16. use App\Entity\IProvidesServices;
  17. use App\Entity\Location\City;
  18. use App\Entity\Location\MapCoordinate;
  19. use App\Entity\Location\Station;
  20. use App\Entity\Messengers;
  21. use App\Entity\PhoneCallRestrictions;
  22. use App\Entity\Profile\Comment\CommentByCustomer;
  23. use App\Entity\Profile\Confirmation\ModerationRequest;
  24. use App\Entity\Sales\Profile\AdBoardPlacement;
  25. use App\Entity\Sales\Profile\PlacementHiding;
  26. use App\Entity\Sales\Profile\TopPlacement;
  27. use App\Entity\Saloon\Saloon;
  28. use App\Entity\ProvidedServiceTrait;
  29. use App\Entity\TakeOutPricing;
  30. use App\Helper\TopCardTrait;
  31. use App\Repository\ProfileRepository;
  32. use Carbon\Carbon;
  33. use Carbon\CarbonImmutable;
  34. use Doctrine\Common\Collections\ArrayCollection;
  35. use Doctrine\Common\Collections\Collection;
  36. use Doctrine\ORM\Mapping as ORM;
  37. use Doctrine\ORM\Mapping\Index;
  38. //use ApiPlatform\Core\Annotation\ApiResource;
  39. //use ApiPlatform\Core\Annotation\ApiFilter;
  40. //use ApiPlatform\Core\Bridge\Doctrine\Orm\Filter\SearchFilter;
  41. //use ApiPlatform\Core\Bridge\Doctrine\Orm\Filter\RangeFilter;
  42. use Gedmo\SoftDeleteable\Traits\SoftDeleteableEntity;
  43. use Symfony\Component\Serializer\Annotation\Groups;
  44. use Gedmo\Mapping\Annotation as Gedmo;
  45. use App\Validator\Constraints\ValidPhoneForCountry as ValidPhoneForCountryAssert;
  46. use App\Validator\Constraints\PhoneNotBlack as PhoneNotBlackAssert;
  47. /**
  48.  * ApiResource(collectionOperations={"get"}, itemOperations={"get"}, normalizationContext={"groups"={"profile"}}, attributes={"pagination_client_enabled"=true, "pagination_client_items_per_page"=true})
  49.  * ApiFilter(SearchFilter::class, properties={"city": "exact", "providedServices": "exact"})
  50.  * ApiFilter(RangeFilter::class, properties={"personParameters.age", "personParameters.height", "personParameters.weight", "personParameters.breastSize", "apartmentsPricing.oneHourPrice"})
  51.  * @ValidPhoneForCountryAssert/ProtocolClass
  52.  * @PhoneNotBlackAssert/ProtocolClass
  53.  */
  54. #[Gedmo\SoftDeleteable(fieldName: "deletedAt", timeAware: true)]
  55. #[ORM\Table(name: 'profiles')]
  56. #[Index(name: 'idx_deleted_at', columns: ['deleted_at'])]
  57. #[Index(name: 'idx_created_at', columns: ['created_at'])]
  58. #[Index(name: 'idx_gender', columns: ['person_gender'])]
  59. #[Index(name: 'idx_apartments_one_hour_price', columns: ['apartments_one_hour_price'])]
  60. #[Index(name: 'idx_is_dummy', columns: ['is_dummy'])]
  61. #[Index(name: 'idx_city_deleted', columns: ['city_id', 'deleted_at'])]
  62. #[Index(name: 'idx_city_deleted_moderation', columns: ['city_id', 'deleted_at', 'moderation_status'])]
  63. #[Index(name: 'idx_city_masseur_deleted', columns: ['city_id', 'is_masseur', 'deleted_at'])]
  64. #[Index(name: 'idx_city_masseur_deleted_moderation', columns: ['city_id', 'is_masseur', 'deleted_at', 'moderation_status'])]
  65. #[Index(name: 'idx_city_deleted_gender', columns: ['city_id', 'deleted_at', 'person_gender'])]
  66. #[Index(name: 'idx_city_deleted_moderation_gender', columns: ['city_id', 'deleted_at', 'moderation_status', 'person_gender'])]
  67. #[Index(name: 'idx_city_masseur_deleted_gender', columns: ['city_id', 'is_masseur', 'deleted_at', 'person_gender'])]
  68. #[Index(name: 'idx_city_masseur_deleted_moderation_gender', columns: ['city_id', 'is_masseur', 'deleted_at', 'moderation_status', 'person_gender'])]
  69. #[Index(name: 'idx_saloon', columns: ['saloon_id'])]
  70. #[ORM\Entity(repositoryClass: ProfileRepository::class)]
  71. #[ORM\HasLifecycleCallbacks]
  72. class Profile implements ContainsDomainEvents, IProvidesServices
  73. {
  74.     use SoftDeleteableEntity;
  75.     use DomainEventsRecorderTrait;
  76.     use ProvidedServiceTrait;
  77.     use TopCardTrait;
  78.     const MODERATION_STATUS_NOT_PASSED = 0;
  79.     const MODERATION_STATUS_APPROVED = 1;
  80.     const MODERATION_STATUS_WAITING = 2;
  81.     const MODERATION_STATUS_REJECTED = 3;
  82.     #[ORM\Id]
  83.     #[ORM\Column(name: 'id', type: 'integer')]
  84.     #[ORM\GeneratedValue(strategy: 'AUTO')]
  85.     #[Groups(['preview', 'listing'])]
  86.     protected int $id;
  87.     #[ORM\JoinColumn(name: 'user_id', referencedColumnName: 'id', nullable: true)]
  88.     #[ORM\ManyToOne(targetEntity: Advertiser::class, inversedBy: 'profiles')]
  89.     protected ?Advertiser $owner;
  90.     #[ORM\Column(name: 'is_dummy', type: 'boolean', options: ['default' => 0])]
  91.     protected bool $dummy = false;
  92.     /** @var TopPlacement[] */
  93.     #[ORM\OneToMany(targetEntity: TopPlacement::class, mappedBy: 'profile', cascade: ['all'], orphanRemoval: true)]
  94.     protected Collection $topPlacements;
  95.     #[ORM\OneToOne(targetEntity: AdBoardPlacement::class, mappedBy: 'profile', cascade: ['all'], orphanRemoval: true)]
  96.     protected ?AdBoardPlacement $adBoardPlacement = null;
  97.     #[ORM\OneToOne(targetEntity: PlacementHiding::class, mappedBy: 'profile', cascade: ['all'])]
  98.     protected ?PlacementHiding $placementHiding = null;
  99.     #[ORM\Column(name: 'uri_identity', type: 'string', length: 64)]
  100.     #[Groups(['preview', 'listing'])]
  101.     protected string $uriIdentity;
  102.     #[ORM\Column(name: 'name', type: 'translatable')]
  103.     #[Groups(['preview', 'listing'])]
  104.     protected TranslatableValue $name;
  105.     #[ORM\Column(name: 'description', type: 'translatable')]
  106.     #[Groups('preview')]
  107.     protected ?TranslatableValue $description = null;
  108.     #[ORM\Embedded(class: PersonParameters::class, columnPrefix: 'person_')]
  109.     #[Groups(['preview', 'listing'])]
  110.     protected PersonParameters $personParameters;
  111.     /** var ProfileService[] */
  112.     #[ORM\OneToMany(targetEntity: ProfileService::class, mappedBy: 'profile', cascade: ['all'], orphanRemoval: true)]
  113.     #[ORM\Cache(usage: 'NONSTRICT_READ_WRITE', region: 'profiles')]
  114.     #[Groups(['preview', 'listing'])]
  115.     protected Collection $providedServices;
  116.     #[ORM\OneToMany(targetEntity: ProfileProvidedInventoryItem::class, mappedBy: 'profile', cascade: ['all'], orphanRemoval: true)]
  117.     #[ORM\Cache(usage: 'NONSTRICT_READ_WRITE', region: 'profiles')]
  118.     #[Groups(['preview', 'listing'])]
  119.     protected Collection $providedInventoryItems;
  120.     /** @var int[] */
  121.     #[ORM\Column(name: 'client_types', type: 'simple_array', nullable: true)]
  122.     #[Groups('preview')]
  123.     protected ?array $clientTypes;
  124.     #[ORM\Column(name: 'phone_number', type: 'string', length: 24)]
  125.     #[Groups(['preview', 'listing'])]
  126.     protected string $phoneNumber;
  127.     #[ORM\Embedded(class: Messengers::class, columnPrefix: false)]
  128.     #[Groups(['preview', 'listing'])]
  129.     protected ?Messengers $messengers = null;
  130.     #[ORM\Embedded(class: PhoneCallRestrictions::class, columnPrefix: false)]
  131.     #[Groups('preview')]
  132.     protected ?PhoneCallRestrictions $phoneCallRestrictions = null;
  133.     #[ORM\Column(name: 'is_masseur', type: 'boolean')]
  134.     #[Groups(['preview', 'listing'])]
  135.     protected bool $masseur = false;
  136.     #[ORM\Embedded(class: ClientRestrictions::class, columnPrefix: false)]
  137.     #[Groups('preview')]
  138.     protected ?ClientRestrictions $clientRestrictions = null;
  139.     #[ORM\Embedded(class: ApartmentsPricing::class, columnPrefix: false)]
  140.     #[Groups('preview')]
  141.     protected ?ApartmentsPricing $apartmentsPricing = null;
  142.     #[ORM\Embedded(class: TakeOutPricing::class, columnPrefix: false)]
  143.     #[Groups('preview')]
  144.     protected ?TakeOutPricing $takeOutPricing = null;
  145.     #[ORM\Embedded(class: ExpressPricing::class, columnPrefix: false)]
  146.     #[Groups('preview')]
  147.     protected ?ExpressPricing $expressPricing = null;
  148.     #[ORM\Embedded(class: CarPricing::class, columnPrefix: false)]
  149.     #[Groups('preview')]
  150.     protected ?CarPricing $carPricing = null;
  151.     #[ORM\Column(name: 'extra_charge', type: 'integer', nullable: true)]
  152.     #[Groups('preview')]
  153.     protected ?int $extraCharge;
  154.     #[ORM\Column(name: 'prepayment', type: 'boolean', nullable: true)]
  155.     #[Groups('preview')]
  156.     protected ?bool $prepayment = null;
  157.     #[ORM\Column(name: 'prepayment_amount', type: 'integer', nullable: true)]
  158.     #[Groups('preview')]
  159.     protected ?int $prepaymentAmount = null;
  160.     #[ORM\Column(name: 'prepayment_comment', type: 'string', length: 512, nullable: true)]
  161.     #[Groups('preview')]
  162.     protected ?string $prepaymentComment = null;
  163.     /** @var Photo[] */
  164.     #[ORM\OneToMany(targetEntity: Photo::class, mappedBy: 'profile', cascade: ['all'], orphanRemoval: true)]
  165.     #[Groups('preview')]
  166.     protected Collection $photos;
  167.     /** @var Selfie[] */
  168.     #[ORM\OneToMany(targetEntity: Selfie::class, mappedBy: 'profile', cascade: ['all'], orphanRemoval: true)]
  169.     #[Groups('preview')]
  170.     protected Collection $selfies;
  171.     /** @var Video[] */
  172.     #[ORM\OneToMany(targetEntity: Video::class, mappedBy: 'profile', cascade: ['all'], orphanRemoval: true)]
  173.     #[Groups('preview')]
  174.     protected Collection $videos;
  175.     #[ORM\OneToMany(targetEntity: FileProcessingTask::class, mappedBy: 'profile', cascade: ['all'], orphanRemoval: true)]
  176.     protected Collection $processingFiles;
  177.     #[ORM\OneToOne(targetEntity: AdminApprovalPhoto::class, mappedBy: 'profile', cascade: ['all'], orphanRemoval: true)]
  178.     protected ?AdminApprovalPhoto $adminApprovalPhoto = null;
  179.     #[ORM\OneToOne(targetEntity: Avatar::class, mappedBy: 'profile', cascade: ['all'], orphanRemoval: true)]
  180.     #[Groups(['preview', 'listing'])]
  181.     protected ?Avatar $avatar = null;
  182.     /** @var CommentByCustomer[] */
  183.     #[ORM\OneToMany(targetEntity: CommentByCustomer::class, mappedBy: 'profile')]
  184.     #[Groups('preview')]
  185.     protected Collection $comments;
  186.     #[ORM\Column(name: 'is_approved', type: 'boolean')]
  187.     #[Groups(['preview', 'listing'])]
  188.     protected bool $approved = false;
  189.     #[ORM\Column(name: 'moderation_status', type: 'integer')]
  190.     #[Groups('preview')]
  191.     protected int $moderationStatus = 0;
  192.     #[ORM\JoinColumn(name: 'city_id', referencedColumnName: 'id')]
  193.     #[ORM\ManyToOne(targetEntity: City::class)]
  194.     #[ORM\Cache(usage: 'NONSTRICT_READ_WRITE', region: 'profiles')]
  195.     #[Groups(['preview', 'listing'])]
  196.     protected City $city;
  197.     /** @var Station[] */
  198.     //, indexBy="id"
  199.     #[ORM\JoinTable(name: 'profile_stations')]
  200.     #[ORM\JoinColumn(name: 'profile_id', referencedColumnName: 'id')]
  201.     #[ORM\InverseJoinColumn(name: 'station_id', referencedColumnName: 'id')]
  202.     #[ORM\ManyToMany(targetEntity: Station::class)]
  203.     #[Groups(['preview', 'listing'])]
  204.     #[ORM\Cache(usage: 'NONSTRICT_READ_WRITE', region: 'profiles')]
  205.     protected Collection $stations;
  206.     /** @var Collection<int, ProfileName> */
  207.     #[ORM\JoinTable(name: 'profile_profile_names')]
  208.     #[ORM\JoinColumn(name: 'profile_id', referencedColumnName: 'id', onDelete: 'CASCADE')]
  209.     #[ORM\InverseJoinColumn(name: 'profile_name_id', referencedColumnName: 'id', onDelete: 'CASCADE')]
  210.     #[ORM\ManyToMany(targetEntity: ProfileName::class, inversedBy: 'profiles')]
  211.     protected Collection $profileNames;
  212.     #[ORM\Embedded(class: MapCoordinate::class, columnPrefix: false)] // ApiProperty()
  213.     #[Groups(['preview', 'listing'])]
  214.     protected ?MapCoordinate $mapCoordinate;
  215.     #[ORM\Column(name: 'created_at', type: 'datetimetz_immutable', nullable: true)]
  216.     #[Groups('preview')]
  217.     protected ?\DateTimeImmutable $createdAt;
  218.     #[Gedmo\Timestampable(on: "change", field: ["name", "description", "personParameters", "providedServices", "clientTypes", "phoneNumber", "messengers", "phoneCallrestrictions", "masseur", "clientRestrictions", "apartmentsPricing", "takeOutPricing", "expressPricing", "carPricing", "extraCharge", "prepayment", "prepaymentAmount", "prepaymentComment", "photos", "selfies", "videos", "avatar", "stations", "mapCoordinate"])]
  219.     #[ORM\Column(name: 'updated_at', type: 'datetimetz_immutable', nullable: true)]
  220.     #[Groups('preview')]
  221.     protected ?\DateTimeImmutable $updatedAt;
  222.     #[ORM\Column(name: 'mautic_stale_30d_sent_at', type: 'datetimetz_immutable', nullable: true)]
  223.     private ?\DateTimeImmutable $mauticStale30dSentAt = null;
  224.     #[ORM\Column(name: 'inactivated_at', type: 'datetimetz_immutable', nullable: true)]
  225.     protected ?\DateTimeImmutable $inactivatedAt;
  226.     private bool $draft = false;
  227.     #[ORM\Column(name: 'seo', type: 'json', nullable: true)]
  228.     #[Groups(['preview', 'listing'])]
  229.     private ?array $seo = null;
  230.     #[ORM\ManyToOne(targetEntity: Station::class)]
  231.     #[ORM\JoinColumn(name: 'primary_station_id', referencedColumnName: 'id', nullable: true, onDelete: 'SET NULL')]
  232.     #[Groups(['preview', 'listing'])]
  233.     #[ORM\Cache(usage: 'READ_ONLY')]
  234.     private ?Station $primaryStation = null;
  235.     #[ORM\ManyToOne(targetEntity: Saloon::class, inversedBy: 'profiles')]
  236.     #[ORM\JoinColumn(name: 'saloon_id', referencedColumnName: 'id', nullable: true, onDelete: 'SET NULL')]
  237.     private ?Saloon $saloon = null;
  238.     #[ORM\Column(type: 'smallint', options: ['default' => 0])]
  239.     private int $deleteMode = 0;
  240.     protected function __construct(?\DateTimeImmutable $createdAt)
  241.     {
  242.         $this->draft = true;
  243.         $this->createdAt = $createdAt;
  244.         $this->photos = new ArrayCollection();
  245.         $this->selfies = new ArrayCollection();
  246.         $this->videos = new ArrayCollection();
  247.         $this->processingFiles = new ArrayCollection();
  248.         $this->comments = new ArrayCollection();
  249.         $this->topPlacements = new ArrayCollection();
  250.         $this->providedServices = new ArrayCollection();
  251.         $this->providedInventoryItems = new ArrayCollection();
  252.         $this->stations = new ArrayCollection();
  253.         $this->profileNames = new ArrayCollection();
  254.         $this->inactivatedAt = CarbonImmutable::now();
  255.     }
  256.     public static function draft(?\DateTimeImmutable $createdAt = null, ?bool $dummy = null): self
  257.     {
  258.         $profile = new static($createdAt);
  259.         if (null !== $dummy)
  260.             $profile->dummy = $dummy;
  261.         return $profile;
  262.     }
  263.     public static function create(string $uriIdentity, ?\DateTimeImmutable $createdAt = null): self
  264.     {
  265.         $profile = new static($createdAt);
  266.         $profile->defineUriIdentity($uriIdentity);
  267.         $profile->toggleMasseur(false);
  268.         return $profile;
  269.     }
  270.     public function defineUriIdentity(string $uriIdentity): void
  271.     {
  272.         if (!$this->isDraft()) {
  273.             throw new \DomainException('Profile is already created and can\'t change its URI.');
  274.         }
  275.         $this->uriIdentity = $uriIdentity;
  276.         $this->draft = false;
  277.     }
  278.     public function isDraft(): bool
  279.     {
  280.         return $this->draft;
  281.     }
  282.     public function toggleMasseur(bool $isMasseur): void
  283.     {
  284.         if ($this->masseur !== $isMasseur && (null !== $this->adBoardPlacement && false == $this->adBoardPlacement->getType()->isFree())) {
  285.             throw new \DomainException('Impossible to toggle profile type while it is displaying on adboard.');
  286.         }
  287.         $this->masseur = $isMasseur;
  288.     }
  289.     public static function createMasseur(string $uriIdentity, ?\DateTimeImmutable $createdAt = null): self
  290.     {
  291.         $profile = new static($createdAt);
  292.         $profile->defineUriIdentity($uriIdentity);
  293.         $profile->toggleMasseur(true);
  294.         return $profile;
  295.     }
  296.     public function isOwnedBy(Advertiser $account): bool
  297.     {
  298.         return $account->getId() === $this->owner->getId();
  299.     }
  300.     public function getId(): int
  301.     {
  302.         return $this->id;
  303.     }
  304.     public function setBio(TranslatableValue $name, TranslatableValue $description): void
  305.     {
  306.         $this->name = $name;
  307.         $this->description = $description;
  308.     }
  309.     public function setLocation(City $city, $stations, ?MapCoordinate $mapCoordinate): void
  310.     {
  311.         if (!$this->isDraft() && !$this->city->equals($city)) {
  312.             throw new \DomainException('City change for a saved profile is forbidden.');
  313.         }
  314.         $this->city = $city;
  315.         $this->changeStations($stations);
  316.         $this->normalizePrimaryStation();
  317.         $this->mapCoordinate = $mapCoordinate;
  318.     }
  319.     protected function changeStations($stations)
  320.     {
  321.         if (null === $stations)
  322.             return;
  323.         if (false === is_array($stations) && false === is_iterable($stations))
  324.             throw new \InvalidArgumentException('Stations list should be either an array or an ArrayCollection');
  325.         $stationsArray = is_iterable($stations) && !is_array($stations) ? iterator_to_array($stations) : $stations;
  326.         $stations = [];
  327.         foreach ($stationsArray as $station) {
  328.             $stations[$station->getId()] = $station;
  329.         }
  330.         $stationIds = array_map(function (Station $station): int {
  331.             return $station->getId();
  332.         }, $stations);
  333.         $existingStationIds = $this->stations->map(function (Station $station): int {
  334.             return $station->getId();
  335.         })->getValues();
  336.         $stationIdsToAdd = array_diff($stationIds, $existingStationIds);
  337.         $stationIdsToRemove = array_diff($existingStationIds, $stationIds);
  338.         foreach ($stationIdsToAdd as $stationId) {
  339.             $this->stations->add($stations[$stationId]);
  340.         }
  341.         foreach ($stationIdsToRemove as $stationId) {
  342.             $this->stations->remove($stationId);
  343.         }
  344.     }
  345.     public function normalizePrimaryStation(): void
  346.     {
  347.         if ($this->stations->isEmpty()) {
  348.             $this->primaryStation = null;
  349.             return;
  350.         }
  351.         if ($this->primaryStation === null || !$this->stations->contains($this->primaryStation)) {
  352.             $this->primaryStation = $this->stations->first();
  353.         }
  354.     }
  355.     public function setEnabledProvidedServices($services): void
  356.     {
  357.         if (null !== $services) {
  358.             if (is_array($services)) {
  359.                 $services = new ArrayCollection($services);
  360.             } elseif (!$services instanceof ArrayCollection) {
  361.                 if (is_iterable($services)) {
  362.                     $services = new ArrayCollection(iterator_to_array($services));
  363.                 } else {
  364.                     throw new \InvalidArgumentException('Services list should be either an array or an ArrayCollection');
  365.                 }
  366.             }
  367.             $this->providedServices = $services;
  368.         }
  369.     }
  370.     public function getProvidedInventoryItems(): Collection
  371.     {
  372.         return $this->providedInventoryItems;
  373.     }
  374.     public function getProvidedInventoryItemsGroupedByType(): array
  375.     {
  376.         $items = $this->providedInventoryItems->toArray();
  377.         usort(
  378.             $items,
  379.             function (ProfileProvidedInventoryItem $left, ProfileProvidedInventoryItem $right): int {
  380.                 return [
  381.                     ProfileInventoryItemType::getSortOrder($left->getItem()->getType()),
  382.                     $left->getItem()->getSort(),
  383.                     $left->getItem()->getId(),
  384.                 ] <=> [
  385.                     ProfileInventoryItemType::getSortOrder($right->getItem()->getType()),
  386.                     $right->getItem()->getSort(),
  387.                     $right->getItem()->getId(),
  388.                 ];
  389.             }
  390.         );
  391.         $groupedItems = [];
  392.         foreach ($items as $providedItem) {
  393.             $groupedItems[$providedItem->getItem()->getType()][] = $providedItem;
  394.         }
  395.         return $groupedItems;
  396.     }
  397.     public function removeProvidedInventoryItem(ProfileProvidedInventoryItem $providedItem): void
  398.     {
  399.         $this->providedInventoryItems->removeElement($providedItem);
  400.     }
  401.     public function provideInventoryItem(ProfileProvidedInventoryItem $providedItem): void
  402.     {
  403.         $existingSameItem = $this->providedInventoryItems->filter(
  404.             function (ProfileProvidedInventoryItem $existingProvidedItem) use ($providedItem): bool {
  405.                 return $existingProvidedItem->getItem()->getId() === $providedItem->getItem()->getId();
  406.             }
  407.         )->first();
  408.         if ($existingSameItem instanceof ProfileProvidedInventoryItem) {
  409.             $existingSameItem->updateComment($providedItem->getComment());
  410.             return;
  411.         }
  412.         $this->providedInventoryItems->add($providedItem);
  413.     }
  414.     public function setPhoneCallOptions(string $phoneNumber, ?PhoneCallRestrictions $restrictions, ?Messengers $messengers): void
  415.     {
  416.         $this->phoneNumber = $phoneNumber;
  417.         $this->phoneCallRestrictions = $restrictions;
  418.         $this->messengers = $messengers;
  419.     }
  420.     public function getSaloon(): ?Saloon
  421.     {
  422.         return $this->saloon;
  423.     }
  424.     public function attachToSaloon(?Saloon $saloon): void
  425.     {
  426.         $this->saloon = $saloon;
  427.     }
  428.     public function detachFromSaloon(): void
  429.     {
  430.         $this->attachToSaloon(null);
  431.     }
  432.     public function setPricing(?ApartmentsPricing $apartmentsPricing, ?TakeOutPricing $takeOutPricing, ?int $extraCharge, ?ExpressPricing $expressPricing = null, ?CarPricing $carPricing = null): void
  433.     {
  434.         $this->apartmentsPricing = $apartmentsPricing;
  435.         $this->takeOutPricing = $takeOutPricing;
  436.         $this->extraCharge = $extraCharge;
  437.         $this->expressPricing = $expressPricing;
  438.         $this->carPricing = $carPricing;
  439.     }
  440.     public function setPrepaymentOptions(?bool $prepayment, ?int $prepaymentAmount, ?string $prepaymentComment): void
  441.     {
  442.         $this->prepayment = $prepayment;
  443.         if ($prepayment !== true) {
  444.             $this->prepaymentAmount = null;
  445.             $this->prepaymentComment = null;
  446.             return;
  447.         }
  448.         $this->prepaymentAmount = $prepaymentAmount;
  449.         $this->prepaymentComment = $prepaymentComment;
  450.     }
  451.     public function isApproved(): bool
  452.     {
  453.         return $this->approved;
  454.     }
  455.     public function approve(): void
  456.     {
  457.         $this->approved = true;
  458.     }
  459.     public function unApprove(): void
  460.     {
  461.         $this->approved = false;
  462.     }
  463.     public function getOwner(): ?Advertiser
  464.     {
  465.         return $this->owner;
  466.     }
  467.     public function setOwner(Advertiser $owner): void
  468.     {
  469.         $this->owner = $owner;
  470.     }
  471.     public function hasOwner(): bool
  472.     {
  473.         return null !== $this->owner;
  474.     }
  475.     public function getTopPlacements(): Collection
  476.     {
  477.         return $this->topPlacements;
  478.     }
  479.     public function addTopPlacement(TopPlacement $topPlacement): void
  480.     {
  481.         $this->topPlacements->add($topPlacement);
  482.     }
  483.     public function getAdBoardPlacement(): ?AdBoardPlacement
  484.     {
  485.         return $this->adBoardPlacement;
  486.     }
  487.     public function setAdBoardPlacement(AdBoardPlacement $adBoardPlacement): void
  488.     {
  489.         $this->adBoardPlacement = $adBoardPlacement;
  490.     }
  491.     /**
  492.      * Анкета оплачена и выводится в общих списках на сайте
  493.      * или в ТОПе, то есть "АКТИВНА"
  494.      */
  495.     public function isActive(): bool
  496.     {
  497.         return null !== $this->adBoardPlacement || $this->hasRunningTopPlacement();
  498.     }
  499.     public function hasRunningTopPlacement(): bool
  500.     {
  501.         $now = new \DateTimeImmutable('now');
  502.         foreach ($this->topPlacements as /** @var TopPlacement $topPlacement */ $topPlacement) {
  503.             if ($topPlacement->getPlacedAt() <= $now && $now <= $topPlacement->getExpiresAt())
  504.                 return true;
  505.         }
  506.         return false;
  507.     }
  508.     public function getUriIdentity(): string
  509.     {
  510.         return $this->uriIdentity;
  511.     }
  512.     public function getName(): TranslatableValue
  513.     {
  514.         return $this->name;
  515.     }
  516.     public function getDescription(): ?TranslatableValue
  517.     {
  518.         return $this->description;
  519.     }
  520.     public function updateDescription(TranslatableValue $description): void
  521.     {
  522.         $this->description = $description;
  523.     }
  524.     public function getPersonParameters(): PersonParameters
  525.     {
  526.         return $this->personParameters;
  527.     }
  528.     public function setPersonParameters(PersonParameters $personParameters): void
  529.     {
  530.         $this->personParameters = $personParameters;
  531.     }
  532.     public function getPhoneNumber(): string
  533.     {
  534.         return $this->phoneNumber;
  535.     }
  536.     //TODO return type
  537.     public function getPhoneCallRestrictions(): ?PhoneCallRestrictions
  538.     {
  539.         return $this->phoneCallRestrictions;
  540.     }
  541.     public function isMasseur(): bool
  542.     {
  543.         return $this->masseur;
  544.     }
  545.     //TODO return type
  546.     public function getClientRestrictions(): ?ClientRestrictions
  547.     {
  548.         return $this->clientRestrictions;
  549.     }
  550.     //TODO return type
  551.     public function setClientRestrictions(?ClientRestrictions $restrictions): void
  552.     {
  553.         $this->clientRestrictions = $restrictions;
  554.     }
  555.     //TODO return type
  556.     public function getApartmentsPricing(): ?ApartmentsPricing
  557.     {
  558.         return $this->apartmentsPricing;
  559.     }
  560.     public function getTakeOutPricing(): ?TakeOutPricing
  561.     {
  562.         return $this->takeOutPricing;
  563.     }
  564.     public function getExtraCharge(): ?int
  565.     {
  566.         return $this->extraCharge;
  567.     }
  568.     public function isPrepayment(): ?bool
  569.     {
  570.         return $this->prepayment;
  571.     }
  572.     public function isWithoutPrepayment(): bool
  573.     {
  574.         return $this->prepayment === false;
  575.     }
  576.     public function getPrepaymentAmount(): ?int
  577.     {
  578.         return $this->prepaymentAmount;
  579.     }
  580.     public function getPrepaymentComment(): ?string
  581.     {
  582.         return $this->prepaymentComment;
  583.     }
  584.     public function addPhoto(string $path, bool $isMain): Photo
  585.     {
  586.         $photos = $this->getPhotos();
  587.         $found = $photos->filter(function (Photo $photo) use ($path): bool {
  588.             return $path === $photo->getPath();
  589.         });
  590.         if (!$found->isEmpty())
  591.             return $found->first();
  592.         if (true === $isMain) {
  593.             $photos->forAll(function ($index, Photo $photo): true {
  594.                 $photo->unsetMain();
  595.                 return true;
  596.             });
  597.         }
  598.         $photo = new Photo($this, $path, $isMain);
  599.         $this->photos->add($photo);
  600.         return $photo;
  601.     }
  602.     /**
  603.      * @return Photo[]
  604.      */
  605.     public function getPhotos(): Collection
  606.     {
  607.         return $this->photos->filter(function ($mediaFile): bool {
  608.             return get_class($mediaFile) == Photo::class;
  609.         });
  610.     }
  611.     public function removePhoto(string $path): bool
  612.     {
  613.         foreach ($this->getPhotos() as $photo) {
  614.             if ($path === $photo->getPath()) {
  615.                 $this->photos->removeElement($photo);
  616.                 return true;
  617.             }
  618.         }
  619.         return false;
  620.     }
  621.     public function getMainPhotoOrFirstPhoto(): ?Photo
  622.     {
  623.         $photos = $this->getPhotos();
  624.         if ($photos->isEmpty()) {
  625.             return null;
  626.         }
  627.         $mainPhoto = $this->getMainPhoto();
  628.         if (null === $mainPhoto) {
  629.             $mainPhoto = $photos->first();
  630.         }
  631.         return $mainPhoto;
  632.     }
  633.     public function getMainPhoto(): ?Photo
  634.     {
  635.         $photos = $this->getPhotos();
  636.         if ($photos->isEmpty()) {
  637.             return null;
  638.         }
  639.         $mainPhoto = null;
  640.         $photos->forAll(function ($index, Photo $photo) use (&$mainPhoto): bool {
  641.             if ($photo->isMain()) {
  642.                 $mainPhoto = $photo;
  643.                 return false; // Stop the cycle
  644.             }
  645.             return true;
  646.         });
  647.         return $mainPhoto;
  648.     }
  649.     public function changeMainPhoto(string $path): void
  650.     {
  651.         $photos = $this->getPhotos();
  652.         $found = $photos->filter(function (Photo $photo) use ($path): bool {
  653.             return $path === $photo->getPath();
  654.         });
  655.         if ($found->isEmpty()) {
  656.             return;
  657.         }
  658.         $mainPhoto = $found->first();
  659.         $photos->forAll(function ($index, Photo $photo): true {
  660.             $photo->unsetMain();
  661.             return true;
  662.         });
  663.         $mainPhoto->setMain();
  664.     }
  665.     public function addSelfie(string $path): Selfie
  666.     {
  667.         $found = $this->getSelfies()->filter(function (Selfie $selfie) use ($path): bool {
  668.             return $path === $selfie->getPath();
  669.         });
  670.         if (!$found->isEmpty())
  671.             return $found->first();
  672.         $selfie = new Selfie($this, $path);
  673.         $this->selfies->add($selfie);
  674.         return $selfie;
  675.     }
  676.     /**
  677.      * @return Selfie[]
  678.      */
  679.     public function getSelfies(): Collection
  680.     {
  681.         return $this->selfies;
  682.     }
  683.     public function removeSelfie(string $path): bool
  684.     {
  685.         foreach ($this->getSelfies() as $selfie) {
  686.             if ($path === $selfie->getPath()) {
  687.                 $this->selfies->removeElement($selfie);
  688.                 return true;
  689.             }
  690.         }
  691.         return false;
  692.     }
  693.     public function getConfirmedVideos(): Collection
  694.     {
  695.         return $this->videos->filter(function ($mediaFile): bool {
  696.             if (!$mediaFile instanceof Video) {
  697.                 return false;
  698.             }
  699.             return $mediaFile->isConfirmed();
  700.         });
  701.     }
  702.     /**
  703.      * Храним только 1 видео для анкеты
  704.      */
  705.     public function addVideo(string $videoPath, ?string $posterPath = null): Video
  706.     {
  707.         $found = $this->getVideos()->filter(function (Video $video) use ($videoPath): bool {
  708.             return $videoPath === $video->getPath();
  709.         });
  710.         if (!$found->isEmpty())
  711.             return $found->first();
  712.         $video = new Video($this, $videoPath);
  713.         if ($this->isEligibleToShowByModeration()) {
  714.             $video->passModeration();
  715.         }
  716.         if (null !== $posterPath) {
  717.             $video->setPreviewPath($posterPath);
  718.         }
  719.         //теперь разрешаем много видео
  720.         //$this->videos->clear();
  721.         $this->videos->add($video);
  722.         return $video;
  723.     }
  724.     /**
  725.      * @return Video[]
  726.      */
  727.     public function getVideos(): Collection
  728.     {
  729.         return $this->videos->filter(function ($mediaFile): bool {
  730.             return ($mediaFile instanceof Video);
  731.         });
  732.     }
  733.     public function removeVideo(string $path): bool
  734.     {
  735.         foreach ($this->getVideos() as $video) {
  736.             if ($path === $video->getPath()) {
  737.                 $this->videos->removeElement($video);
  738.                 $this->photos->removeElement($video);
  739.                 return true;
  740.             }
  741.         }
  742.         return false;
  743.     }
  744.     /**
  745.      * Добавляет таск на обработку оригинала видео в подходящий формат
  746.      *
  747.      * @param string $path Путь к файлу оригинала относительно фс очередей
  748.      */
  749.     public function addRawVideo(string $path): FileProcessingTask
  750.     {
  751.         $file = new FileProcessingTask($this, $path);
  752.         $this->processingFiles->add($file);
  753.         return $file;
  754.     }
  755.     public function hasFilesInProcess(): bool
  756.     {
  757.         return $this->videosInProcess() > 0;
  758.     }
  759.     public function videosInProcess(): int
  760.     {
  761.         $inProcess = $this->processingFiles->filter(function (FileProcessingTask $task): bool {
  762.             return !$task->isCompleted();
  763.         });
  764.         return $inProcess->count();
  765.     }
  766.     public function isMediaProcessed(): bool
  767.     {
  768.         foreach ($this->videos as $video)
  769.             if (null === $video->getPreviewPath())
  770.                 return false;
  771.         return true;
  772.     }
  773.     public function getAvatar(): ?Avatar
  774.     {
  775.         return $this->avatar;
  776.     }
  777.     public function setAvatar(string $path): void
  778.     {
  779.         $this->avatar = new Avatar($this, $path);
  780.     }
  781.     public function removeAvatar(): bool
  782.     {
  783.         if (null == $this->avatar)
  784.             return false;
  785.         foreach ($this->photos as $photo) {
  786.             if ($this->avatar->getPath() === $photo->getPath()) {
  787.                 $this->photos->removeElement($photo);
  788.                 break;
  789.             }
  790.         }
  791.         $this->avatar = null;
  792.         return true;
  793.     }
  794.     /**
  795.      * @return CommentByCustomer[]
  796.      */
  797.     public function getComments(): Collection
  798.     {
  799.         return $this->comments->filter(function (CommentByCustomer $comment): bool {
  800.             return null == $comment->getParent();
  801.         });
  802.     }
  803.     /**
  804.      * @return CommentByCustomer[]
  805.      */
  806.     public function getCommentsOrderedByNotReplied(): array
  807.     {
  808.         $comments = $this->comments->filter(function (CommentByCustomer $comment): bool {
  809.             return null == $comment->getParent();
  810.         })->toArray();
  811.         usort($comments, function (CommentByCustomer $commentA, CommentByCustomer $commentB): int {
  812.             if ((null == $commentA->getLastCommentByAdvertiser() && null == $commentB->getLastCommentByAdvertiser())
  813.                 || (null != $commentA->getLastCommentByAdvertiser() && null != $commentB->getLastCommentByAdvertiser())) {
  814.                 if ($commentA->getCreatedAt() == $commentB->getCreatedAt())
  815.                     return $commentA->getId() > $commentB->getId() ? -1 : 1;
  816.                 else
  817.                     return $commentA->getCreatedAt() > $commentB->getCreatedAt() ? -1 : 1;
  818.             }
  819.             if (null == $commentA->getLastCommentByAdvertiser() && null != $commentB->getLastCommentByAdvertiser())
  820.                 return -1;
  821.             else
  822.                 return 1;
  823.         });
  824.         return $comments;
  825.     }
  826.     public function getCreatedAt(): ?\DateTimeImmutable
  827.     {
  828.         return $this->createdAt;
  829.     }
  830.     /**
  831.      * @return CommentByCustomer[]
  832.      */
  833.     public function getCommentsWithoutReply(): Collection
  834.     {
  835.         return $this->comments->filter(function (CommentByCustomer $comment): bool {
  836.             return null == $comment->getParent() && false == $comment->isCommentedByAdvertiser();
  837.         });
  838.     }
  839.     /**
  840.      * @return CommentByCustomer[]
  841.      */
  842.     public function getCommentsWithReply(): Collection
  843.     {
  844.         return $this->comments->filter(function (CommentByCustomer $comment): bool {
  845.             return null == $comment->getParent() && true == $comment->isCommentedByAdvertiser();
  846.         });
  847.     }
  848.     /**
  849.      * @return CommentByCustomer[]
  850.      */
  851.     public function getNewComments(): Collection
  852.     {
  853.         $weekAgo = CarbonImmutable::now()->sub('7 days');
  854.         return $this->comments->filter(function (CommentByCustomer $comment) use ($weekAgo): bool {
  855.             return null == $comment->getParent()
  856.                 && (
  857.                     $comment->getCreatedAt() >= $weekAgo
  858.                     || null == $this->getCommentReply($comment)
  859.                 );
  860.         });
  861.     }
  862.     private function getCommentReply(CommentByCustomer $parent): ?CommentByCustomer
  863.     {
  864.         foreach ($this->comments as $comment)
  865.             if ($comment->getParent() == $parent)
  866.                 return $comment;
  867.         return null;
  868.     }
  869.     public function getOldComments(): Collection
  870.     {
  871.         $weekAgo = CarbonImmutable::now()->sub('7 days');
  872.         return $this->comments->filter(function (CommentByCustomer $comment) use ($weekAgo): bool {
  873.             return null == $comment->getParent()
  874.                 && false == (
  875.                     $comment->getCreatedAt() >= $weekAgo
  876.                     || null == $this->getCommentReply($comment)
  877.                 );
  878.         });
  879.     }
  880.     public function getCommentFromUser(Customer $user): ?CommentByCustomer
  881.     {
  882.         foreach ($this->comments as $comment)
  883.             if (null == $comment->getParent() && null != $comment->getUser() && $user->getId() == $comment->getUser()->getId())
  884.                 return $comment;
  885.         return null;
  886.     }
  887.     //TODO return type
  888.     public function getCity(): City
  889.     {
  890.         return $this->city;
  891.     }
  892.     /** @return Collection<int, ProfileName> */
  893.     public function getProfileNames(): Collection
  894.     {
  895.         return $this->profileNames;
  896.     }
  897.     /**
  898.      * @return Station[]
  899.      */
  900.     public function getStations(): Collection
  901.     {
  902.         return $this->stations;
  903.     }
  904.     public function getMapCoordinate(): ?MapCoordinate
  905.     {
  906.         return $this->mapCoordinate;
  907.     }
  908.     public function getUpdatedAt(): ?\DateTimeImmutable
  909.     {
  910.         return $this->updatedAt;
  911.     }
  912.     public function setUpdatedAt(\DateTimeImmutable $updatedAt): void
  913.     {
  914.         $this->updatedAt = $updatedAt;
  915.         $this->mauticStale30dSentAt = null;
  916.     }
  917.     public function getMauticStale30dSentAt(): ?\DateTimeImmutable
  918.     {
  919.         return $this->mauticStale30dSentAt;
  920.     }
  921.     public function setMauticStale30dSentAt(?\DateTimeImmutable $sentAt): void
  922.     {
  923.         $this->mauticStale30dSentAt = $sentAt;
  924.     }
  925.     public function getModerationStatus(): int
  926.     {
  927.         return $this->moderationStatus;
  928.     }
  929.     public function setModerationStatus(int $status): void
  930.     {
  931.         if (self::MODERATION_STATUS_APPROVED === $status) {
  932.             throw new \RuntimeException(sprintf('Use %s::passModeration() method instead', static::class));
  933.         }
  934.         $validStatuses = [self::MODERATION_STATUS_NOT_PASSED, self::MODERATION_STATUS_APPROVED, self::MODERATION_STATUS_WAITING, self::MODERATION_STATUS_REJECTED];
  935.         if (false === array_search($status, $validStatuses))
  936.             throw new \LogicException('Trying to set an invalid moderation status');
  937.         $this->moderationStatus = $status;
  938.     }
  939.     public function isModerationPassed(): bool
  940.     {
  941.         return $this->moderationStatus == self::MODERATION_STATUS_APPROVED;
  942.     }
  943.     public function isModerationWaiting(): bool
  944.     {
  945.         return $this->moderationStatus == self::MODERATION_STATUS_WAITING;
  946.     }
  947.     public function isModerationRejected(): bool
  948.     {
  949.         return $this->moderationStatus == self::MODERATION_STATUS_REJECTED;
  950.     }
  951.     public function isEligibleToShowByModeration(): bool
  952.     {
  953.         return $this->isModerationPassed()
  954.             || ($this->isModerationWaiting() && null !== $this->owner && $this->owner->isTrusted());
  955.     }
  956.     public function passModeration(?ModerationRequest $moderationRequest = null): void
  957.     {
  958.         $this->moderationStatus = self::MODERATION_STATUS_APPROVED;
  959.         $func = static function ($k, Photo|Video $file) use ($moderationRequest): bool {
  960.             if (!$file->isConfirmed()) {
  961.                 $file->passModeration($moderationRequest);
  962.             }
  963.             return true;
  964.         };
  965.         $this->videos->forAll($func);
  966.     }
  967.     public function delete(): void
  968.     {
  969.         $this->deletePlacementHiding();
  970.         $this->deleteFromAdBoard();
  971.         $now = new \DateTimeImmutable('now');
  972.         $toDelete = [];
  973.         foreach ($this->topPlacements as $topPlacement) {
  974.             if ($topPlacement->getExpiresAt() > $now)
  975.                 $toDelete[] = $topPlacement;
  976.         }
  977.         foreach ($toDelete as $topPlacement)
  978.             $this->topPlacements->removeElement($topPlacement);
  979.         $this->setDeletedAt(Carbon::now());
  980.     }
  981.     public function deletePlacementHiding(): void
  982.     {
  983.         $this->placementHiding = null;
  984.     }
  985.     public function deleteFromAdBoard(): void
  986.     {
  987.         $this->adBoardPlacement = null;
  988.     }
  989.     //TODO return type
  990.     public function undoDelete(): void
  991.     {
  992.         $this->setDeletedAt(); // will pass null by default
  993.     }
  994.     //TODO return type
  995.     public function deleteFromTopPlacement(): void
  996.     {
  997.         //здесь нужна логика отмены конретного размещения
  998. //        $this->topPlacement = null;
  999.     }
  1000.     public function getExpressPricing(): ?ExpressPricing
  1001.     {
  1002.         return $this->expressPricing;
  1003.     }
  1004.     public function getCarPricing(): ?CarPricing
  1005.     {
  1006.         return $this->carPricing;
  1007.     }
  1008.     //TODO return type
  1009.     /**
  1010.      * @return int[]
  1011.      */
  1012.     public function getClientTypes(): array
  1013.     {
  1014.         return $this->clientTypes ?? [];
  1015.     }
  1016.     /**
  1017.      * @param int[] $clientTypes
  1018.      */
  1019.     public function setClientTypes(array $clientTypes): void
  1020.     {
  1021.         $this->clientTypes = $clientTypes;
  1022.     }
  1023.     public function getMessengers(): ?Messengers
  1024.     {
  1025.         return $this->messengers;
  1026.     }
  1027.     public function getInactivatedAt(): ?\DateTimeImmutable
  1028.     {
  1029.         return $this->inactivatedAt;
  1030.     }
  1031.     public function setInactive(): void
  1032.     {
  1033.         $this->inactivatedAt = CarbonImmutable::now();
  1034.     }
  1035.     public function undoInactive(): void
  1036.     {
  1037.         $this->inactivatedAt = null;
  1038.     }
  1039.     public function isHidden(): bool
  1040.     {
  1041.         return null !== $this->getPlacementHiding();
  1042.     }
  1043.     public function getPlacementHiding(): ?PlacementHiding
  1044.     {
  1045.         return $this->placementHiding;
  1046.     }
  1047.     public function setPlacementHiding(PlacementHiding $placementHiding): void
  1048.     {
  1049.         $this->placementHiding = $placementHiding;
  1050.     }
  1051.     public function hasSelfie(): bool
  1052.     {
  1053.         return $this->selfies->count() > 0;
  1054.     }
  1055.     public function hasVideo(): bool
  1056.     {
  1057.         return $this->videos->count() > 0;
  1058.     }
  1059.     public function isCommented(): bool
  1060.     {
  1061.         return $this->comments->count() > 0;
  1062.     }
  1063.     public function adminApprovalPhoto(): ?AdminApprovalPhoto
  1064.     {
  1065.         return $this->adminApprovalPhoto;
  1066.     }
  1067.     public function setAdminApprovalPhoto(?string $path): void
  1068.     {
  1069.         $this->adminApprovalPhoto = $path ? new AdminApprovalPhoto($this, $path) : null;
  1070.     }
  1071.     public function seo(): ?array
  1072.     {
  1073.         return $this->seo;
  1074.     }
  1075.     public function seoPhoneNumber(): ?string
  1076.     {
  1077.         return $this->seo['phone'] ?? null;
  1078.     }
  1079.     public function setSeoPhoneNumber(string $phoneNumber): void
  1080.     {
  1081.         if (null === $this->seo) {
  1082.             $this->seo = [];
  1083.         }
  1084.         $this->seo['phone'] = $phoneNumber;
  1085.     }
  1086.     public function getPrimaryStation(): ?Station
  1087.     {
  1088.         $station = $this->primaryStation ?? $this->getStations()->first();
  1089.         if (false === $station) {
  1090.             return null;
  1091.         }
  1092.         return $station;
  1093.     }
  1094.     public function setPrimaryStation(?Station $station): void
  1095.     {
  1096.         $this->primaryStation = $station;
  1097.         $this->normalizePrimaryStation();
  1098.     }
  1099.     public function getStationsSortedByPrimary(): array
  1100.     {
  1101.         $stations = $this->stations->toArray();
  1102.         if (!$this->primaryStation) {
  1103.             return $stations;
  1104.         }
  1105.         usort($stations, function (Station $a, Station $b) {
  1106.             if ($a->getId() === $this->primaryStation->getId()) return -1;
  1107.             if ($b->getId() === $this->primaryStation->getId()) return 1;
  1108.             return 0;
  1109.         });
  1110.         return $stations;
  1111.     }
  1112.     public function getDistricts(): array
  1113.     {
  1114.         $districts = [];
  1115.         $addedDistricts = [];
  1116.         foreach ($this->getStationsSortedByPrimary() as $station) {
  1117.             $district = $station->getDistrict();
  1118.             if (array_key_exists($district->getId(), $addedDistricts)) {
  1119.                 continue;
  1120.             }
  1121.             $districts[] = $district;
  1122.             $addedDistricts[$district->getId()] = true;
  1123.         }
  1124.         return $districts;
  1125.     }
  1126.     public function getDeleteMode(): int
  1127.     {
  1128.         return $this->deleteMode;
  1129.     }
  1130.     public function setDeleteMode(int $deleteMode): self
  1131.     {
  1132.         $this->deleteMode = $deleteMode;
  1133.         return $this;
  1134.     }
  1135.     public function isHardDeleted(): bool
  1136.     {
  1137.         return $this->deleteMode === 2;
  1138.     }
  1139. }