<?php
namespace App\Entity;
use App\Entity\Enum\StorePriorityEnum;
use App\Utils\Formatter;
use DateTime;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;
use Symfony\Component\Validator\Context\ExecutionContextInterface;
use Wits\PaymentBundle\Entity\PaymentMethod;
use Symfony\Component\HttpFoundation\File\File;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Validator\Constraints as Assert;
use Gedmo\Mapping\Annotation as Gedmo;
use Vich\UploaderBundle\Mapping\Annotation as Vich;
use ApiPlatform\Core\Annotation\ApiResource;
use Symfony\Component\Serializer\Annotation\Groups;
/**
* @ORM\Entity(repositoryClass="App\Repository\StoreRepository")
* @ORM\EntityListeners({"App\Listener\StoreListener"})
* @Gedmo\SoftDeleteable(fieldName="deletedAt")
* @Vich\Uploadable
*
* @ApiResource(
* collectionOperations={
* "get",
* "search_stores"={
* "method"="GET",
* "path"="/stores/search",
* "controller"=App\Controller\Api\SearchStoresAction::class,
* "read"=true
* }
* },
* itemOperations={"get"},
* normalizationContext={"groups"={"store:read"}},
* denormalizationContext={"groups"={"store:write"}}
* )
*/
class Store
{
const STATE_NEW = 'new';
const STATE_VALID = 'valid';
const STATE_REJECTED = 'rejected';
const STATE_TOBEDELETED = 'to_be_deleted';
const AREA_SEARCH = 20; // perimetre de recherche des stores par défaut (reach)
const AREA_SEARCH_EXT = 100; // perimetre de recherche étendu si aucun store (reach extends)
const NUM_STORE_PER_PAGE = 10; // nombre de store par page (finder)
const NUM_TOP_CLIENT_STORE = 3; // nombre de store dans la selection MCA (top page)
// paramètres d'application de l'algorithme de tri des centres (SAW)
const WEIGHT_DISTANCE = 0.1;
const WEIGHT_LEAD_COST = 0;
const WEIGHT_RATIO_CAPPING = 0.9;
/**
* @ORM\Id()
* @ORM\GeneratedValue()
* @ORM\Column(type="integer")
* @Groups({"store:read", "pos:read"})
*/
private $id;
/**
* @ORM\Column(type="string", length=25)
*/
private $state = self::STATE_NEW;
/**
* @ORM\Column(type="string", length=255)
* @Groups({"store:read", "pos:read"})
*/
private $name;
/**
* @ORM\Column(type="string", length=255, nullable=true)
* @Groups({"pos:read", "store:read"})
*/
private $address;
/**
* @ORM\Column(type="string", length=255, nullable=true)
*/
private $addressDetail;
/**
* @ORM\Column(type="string", length=5)
* @Assert\Regex(
* pattern="/^(([0-8][0-9])|(9[0-5])|(2[ab]))[0-9]{3}$/",
* message="Le code postal n'est pas valide"
* )
* @Groups({"pos:read", "store:read"})
*/
private $zipCode;
/**
* @ORM\Column(type="string", length=255)
* @Groups({"pos:read", "store:read"})
*/
private $city;
/**
* @Assert\Regex(pattern="/^(?:\+33|0)\s*[1-9](?:[\s.-]*\d{2}){4}$/", message="Le numéro de téléphone n'est pas valide.")
* @ORM\Column(type="string", length=255, nullable=true)
*/
private $phoneNumber;
/**
* @ORM\Column(type="string", length=255, nullable=true)
*/
private $fax;
/**
* @ORM\ManyToMany(targetEntity="App\Entity\Owner", mappedBy="store")
*/
private $owner;
/**
* @ORM\ManyToOne(targetEntity="App\Entity\HearingBrand", inversedBy="stores")
* @ORM\JoinColumn(nullable=true)
*/
private $hearingBrand;
/**
* @ORM\Column(type="float", nullable=true)
*/
private $latitude;
/**
* @ORM\Column(type="float", nullable=true)
*/
private $longitude;
/**
* @ORM\ManyToOne(targetEntity="App\Entity\Administrator", inversedBy="stores")
*/
private $administrator;
/**
* @ORM\OneToMany(targetEntity="App\Entity\ProspectOnStore", mappedBy="store")
*/
private $prospectsOnStore;
/**
* @ORM\OneToMany(targetEntity="App\Entity\StoreManagementRequest", mappedBy="store", cascade={"persist"})
*/
private $managementRequests;
/**
* @ORM\Column(type="datetime", nullable=true)
*/
private $deletedAt;
/**
* @var bool
* @ORM\Column(type="boolean", options={"default" : false})
*/
private $acquisitionEnabled = false;
/**
* @var float
* @ORM\Column(type="float", nullable=true)
*/
private $reach;
/**
* @var bool
* @ORM\Column(type="integer", options={"default" : 0})
*/
private int $priority = 0;
/**
* @ORM\Column(type="string", length=255, nullable=true)
*/
private $logoPath;
/**
* @assert\File( mimeTypes = {"image/jpeg", "image/png", "image/gif", "image/jpg", "image/webp"})
* @Vich\UploadableField(mapping="store_logo", fileNameProperty="logoPath")
* @var File
*/
private $logoFile;
/**
* @ORM\Column(type="string", length=25, nullable=true)
*/
private $siretNumber;
/**
* @ORM\Column(type="datetime", nullable=true)
* @var DateTime
*/
private $updatedAt;
/**
* @ORM\OneToOne(targetEntity="App\Entity\EstimatedReach")
* @ORM\JoinColumn(nullable=true)
*/
private $estimatedReach;
/**
* String w/ multiple email adresses, splitted with ';'
* @ORM\Column(type="string", length=255, nullable=true)
*/
private $emailNotification;
/**
* @ORM\Column(type="boolean", nullable=true)
*/
private $isEmailNotification;
/**
* @ORM\OneToMany(targetEntity=StoreCentralPurchasingRelation::class, mappedBy="store", orphanRemoval=true)
*/
private $storeCentralPurchasingRelations;
/**
* @ORM\OneToOne(targetEntity=StorePage::class, inversedBy="store")
*/
private $storePage;
/**
* @var string
*
* @Gedmo\Slug(fields={"name","city", "zipCode"})
* @ORM\Column(type="string", length=255, nullable=false, unique=false)
*/
private $slug;
/**
* Teaser (short text) to help promote this store during qualification
* ex:
* Le centre se situe dans le centre commercial des terrasses du port.
* Ce centre commercialise du haut de gamme à des prix très attractifs.
* @Assert\Length(
* min = 2,
* max = 255
* )
* @ORM\Column(type="string", length=255, nullable=true)
* @Groups({"store:read"})
*/
private $teaser;
/**
* @ORM\Column(type="boolean", nullable=true)
*/
private $handicapAccess;
/**
* @ORM\OneToOne(targetEntity=PaymentMethod::class, cascade={"persist", "remove"})
*/
private $paymentMethod;
/**
* @ORM\Column(type="boolean")
*/
private bool $qualificationEnabled;
/**
* @ORM\OneToMany(targetEntity=StoreIndexation::class, mappedBy="store", orphanRemoval=true)
* @ORM\OrderBy ({"endAt" = "ASC"})
*/
private Collection $storeIndexations;
/**
* @ORM\OneToOne(targetEntity=StorePlaceData::class, mappedBy="store", cascade={"persist", "remove"})
*/
private ?StorePlaceData $storePlaceData;
public function __construct()
{
$this->owner = new ArrayCollection();
$this->prospectsOnStore = new ArrayCollection();
$this->managementRequests = new ArrayCollection();
$this->storeCentralPurchasingRelations = new ArrayCollection();
$this->qualificationEnabled = true;
$this->storeIndexations = new ArrayCollection();
}
/**
* Représente l'entité par défault
* @return string
*/
public function __toString()
{
$string = '';
if (!is_null($this->deletedAt)) {
$string .= ' SUPPRIME ';
} else {
$string .= 'Store ' . $this->getName();
}
$string .= ' [' . $this->getId() . ']';
return $string;
}
/**
* @Assert\Callback()
*/
public function validate(ExecutionContextInterface $context, $payload)
{
//acquisition can NOT be activated if state not valid
if ($this->acquisitionEnabled && !$this->isStateValid()) {
$context->buildViolation('L\'acquisition de lead n\'est pas possible tant que le centre n\'a pas été validé')
->atPath('acquisitionEnabled')
->addViolation();
}
}
public function getName(): ?string
{
if (!is_null($this->deletedAt)) {
return $this->name . ' (supprimé)';
}
return $this->name;
}
public function setName(string $name): self
{
$this->name = $name;
return $this;
}
public function getId(): ?int
{
return $this->id;
}
/**
* Représente l'entité dans le BO
* @return string
*/
public function getLabel()
{
return $this->getName();
}
/**
* @Groups({"store:read"})
* @return string
*/
public function getNameAndAddress()
{
return $this->name . ' (' . $this->getAddress() . ', ' . $this->getZipCode() . ', ' . iconv("UTF-8", "UTF-8//IGNORE", $this->getCity()) . ')';
}
public function getAddress(): ?string
{
return $this->address;
}
public function setAddress(?string $address): self
{
$this->address = $address;
return $this;
}
public function getCity(): ?string
{
return strtoupper($this->city);
}
public function setCity(?string $city): self
{
$this->city = $city;
return $this;
}
public function getNameAndZipcodeAndId(): ?string
{
return $this->getNameAndZipcode() . ' [' . $this->getId() . ']';
}
public function getNameAndZipcode()
{
return $this->getName() . ' (' . $this->getZipCode() . ')';
}
public function getZipCode(): ?string
{
return $this->zipCode;
}
public function setZipCode(?string $zipCode): self
{
if (strlen($zipCode) === 4) {
$this->zipCode = str_pad($zipCode, 5, '0', STR_PAD_LEFT);
} else {
$this->zipCode = $zipCode;
}
return $this;
}
/**
* Obtenir l'adresse complète
* @return string
*/
public function getFullAddress()
{
return $this->getAddress() . ' ' . $this->getZipCode() . ' ' . $this->getCity();
}
public function getAddressDetail(): ?string
{
return $this->addressDetail;
}
public function setAddressDetail(?string $addressDetail): self
{
$this->addressDetail = $addressDetail;
return $this;
}
public function getPhoneNumberFormatted(): ?string
{
$phoneNumber = $this->phoneNumber;
$formattedPhoneNumber = chunk_split($phoneNumber, 2, ' ');;
return $formattedPhoneNumber;
}
public function getPhoneNumber(): ?string
{
return $this->phoneNumber;
}
public function setPhoneNumber(?string $phoneNumber): self
{
$phoneNumber = Formatter::phoneNumberFrenchFormat($phoneNumber);
$this->phoneNumber = $phoneNumber;
return $this;
}
public function getFax(): ?string
{
return $this->fax;
}
public function setFax(?string $fax): self
{
$this->fax = $fax;
return $this;
}
/**
* @return Collection|Owner[]
*/
public function getOwners(): Collection
{
return $this->owners;
}
public function addOwner(Owner $owner): self
{
if (!$this->owners->contains($owner)) {
$this->owners[] = $owner;
$owner->setStore($this);
}
return $this;
}
public function removeOwner(Owner $owner): self
{
if ($this->owners->contains($owner)) {
$this->owners->removeElement($owner);
// set the owning side to null (unless already changed)
if ($owner->getStore() === $this) {
$owner->setStore(null);
}
}
return $this;
}
/**
* @return Collection|Prospect[]
*/
public function getProspects(): Collection
{
return $this->prospects;
}
public function addProspect(Prospect $prospect): self
{
if (!$this->prospects->contains($prospect)) {
$this->prospects[] = $prospect;
$prospect->addStore($this);
}
return $this;
}
public function removeProspect(Prospect $prospect): self
{
if ($this->prospects->contains($prospect)) {
$this->prospects->removeElement($prospect);
$prospect->removeStore($this);
}
return $this;
}
public function getHearingBrand(): ?HearingBrand
{
return $this->hearingBrand;
}
public function setHearingBrand(?HearingBrand $hearingBrand): self
{
$this->hearingBrand = $hearingBrand;
return $this;
}
public function getLatitude()
{
return $this->latitude;
}
public function setLatitude($latitude): self
{
$this->latitude = $latitude;
return $this;
}
public function getLongitude()
{
return $this->longitude;
}
public function setLongitude($longitude): self
{
$this->longitude = $longitude;
return $this;
}
public function hasLatitudeAndLongitude()
{
return !is_null($this->longitude) && !is_null($this->latitude);
}
/**
* @return Collection|Owner[]
*/
public function getOwner(): Collection
{
return $this->owner;
}
/**
* @return Collection|ProspectOnStore[]
*/
public function getProspectsOnStore(): Collection
{
return $this->prospectsOnStore;
}
public function addProspectsOnStore(ProspectOnStore $prospectsOnStore): self
{
if (!$this->prospectsOnStore->contains($prospectsOnStore)) {
$this->prospectsOnStore[] = $prospectsOnStore;
$prospectsOnStore->setStore($this);
}
return $this;
}
public function removeProspectsOnStore(ProspectOnStore $prospectsOnStore): self
{
if ($this->prospectsOnStore->contains($prospectsOnStore)) {
$this->prospectsOnStore->removeElement($prospectsOnStore);
// set the owning side to null (unless already changed)
if ($prospectsOnStore->getStore() === $this) {
$prospectsOnStore->setStore(null);
}
}
return $this;
}
public function addManagementRequests(StoreManagementRequest $managementRequest): self
{
if (!$this->managementRequests->contains($managementRequest)) {
$this->managementRequests[] = $managementRequest;
$managementRequest->setStore($this);
}
return $this;
}
public function getState(): ?string
{
return $this->state;
}
public function setState(string $state): self
{
$this->state = $state;
return $this;
}
public function isStateValid(): ?string
{
return $this->state == self::STATE_VALID;
}
public function getDeletedAt(): ?\DateTimeInterface
{
return $this->deletedAt;
}
public function setDeletedAt(?\DateTimeInterface $deletedAt): self
{
$this->deletedAt = $deletedAt;
return $this;
}
/**
* @return Collection|StoreManagementRequest[]
*/
public function getManagementRequests(): Collection
{
return $this->managementRequests;
}
public function addManagementRequest(StoreManagementRequest $managementRequest): self
{
if (!$this->managementRequests->contains($managementRequest)) {
$this->managementRequests[] = $managementRequest;
$managementRequest->setStore($this);
}
return $this;
}
public function removeManagementRequest(StoreManagementRequest $managementRequest): self
{
if ($this->managementRequests->contains($managementRequest)) {
$this->managementRequests->removeElement($managementRequest);
// set the owning side to null (unless already changed)
if ($managementRequest->getStore() === $this) {
$managementRequest->setStore(null);
}
}
return $this;
}
public function getReach(): ?float
{
return $this->reach;
}
public function setReach(float $reach): self
{
$this->reach = $reach;
return $this;
}
public function getIsPriority(): ?bool
{
return $this->priority > 0;
}
public function getPriority(): int
{
return $this->priority;
}
public function getPriorityLabel(): string
{
return StorePriorityEnum::getPriorityName($this->priority);
}
public function setPriority(int $priority): self
{
$this->priority = $priority;
return $this;
}
public function getLogoFile()
{
return $this->logoFile;
}
public function setLogoFile(File $image = null)
{
$this->logoFile = $image;
//update timestamp
if ($image) {
$this->updatedAt = new DateTime('now');
}
return $this;
}
public function hasLogo()
{
return !empty($this->getLogoPath());
}
public function getLogoPath(): ?string
{
return $this->logoPath;
}
public function setLogoPath(string $logoPath = null): self
{
$this->logoPath = $logoPath;
return $this;
}
public function getUpdatedAt(): ?\DateTimeInterface
{
return $this->updatedAt;
}
public function setUpdatedAt(?\DateTimeInterface $updatedAt): self
{
$this->updatedAt = $updatedAt;
return $this;
}
public function getSiretNumber(): ?string
{
return $this->siretNumber;
}
public function setSiretNumber(?string $siretNumber): self
{
$siretNumber = str_replace(' ', '', $siretNumber);
$this->siretNumber = $siretNumber;
return $this;
}
public function getEmailList(): array
{
// Optim: use constant for delimiter?
return explode(";", $this->getEmailNotification());
}
public function getEmailNotification(): ?string
{
return $this->emailNotification;
}
public function mustBeNotifiedByEmail(): ?bool
{
return $this->getIsEmailNotification() && !empty($this->getEmailNotification());
}
public function setEmailNotification(?string $email): self
{
$this->emailNotification = $email;
return $this;
}
public function getEstimatedReach(): ?EstimatedReach
{
return $this->estimatedReach;
}
public function setEstimatedReach(EstimatedReach $estimatedReach): self
{
$this->estimatedReach = $estimatedReach;
return $this;
}
public function getIsEmailNotification(): ?bool
{
return $this->isEmailNotification;
}
// Returns an array with every mail addresses for a store
public function setIsEmailNotification(?bool $isEmailNotification): self
{
$this->isEmailNotification = $isEmailNotification;
return $this;
}
public function isOpenedForProspect(): bool
{
if (!$this->isIndexable()) {
return false;
}
$isAcquisitionEnabled = $this->getAcquisitionEnabled();
//business model
$businessModelPerformance = !is_null($this->getAdministrator()) && $this->administrator->isPerformance();
//if not performance, search for credit
if (!$businessModelPerformance) {
//adminIsActive : partner or freelance with credit
$adminIsActive = !is_null($this->getAdministrator()) && $this->getAdministrator()->isCustomer();
$associatedToCentralPurchasingWithCredit = false;
//is not active, search for active centralPurchasing
if (!$adminIsActive) {
foreach ($this->getStoreCentralPurchasingRelations() as $cpr) {
if ($cpr->isEnabled() && $cpr->getCentralPurchasing()->getNbCredit() > 0) {
$associatedToCentralPurchasingWithCredit = true;
break;
}
}
}
}
return $isAcquisitionEnabled && ($businessModelPerformance || ($adminIsActive || $associatedToCentralPurchasingWithCredit));
}
public function getAcquisitionEnabled(): ?bool
{
return $this->acquisitionEnabled;
}
public function setAcquisitionEnabled(bool $acquisitionEnabled): self
{
$this->acquisitionEnabled = $acquisitionEnabled;
return $this;
}
public function getAdministrator(): ?Administrator
{
return $this->administrator;
}
public function setAdministrator(?Administrator $administrator): self
{
$this->administrator = $administrator;
return $this;
}
/**
* @return Collection|StoreCentralPurchasingRelation[]
*/
public function getStoreCentralPurchasingRelations(): Collection
{
return $this->storeCentralPurchasingRelations;
}
public function addStoreCentralPurchasingRelation(StoreCentralPurchasingRelation $storeCentralPurchasingRelation): self
{
if (!$this->storeCentralPurchasingRelations->contains($storeCentralPurchasingRelation)) {
$this->storeCentralPurchasingRelations[] = $storeCentralPurchasingRelation;
$storeCentralPurchasingRelation->setStore($this);
}
return $this;
}
public function removeStoreCentralPurchasingRelation(StoreCentralPurchasingRelation $storeCentralPurchasingRelation): self
{
if ($this->storeCentralPurchasingRelations->contains($storeCentralPurchasingRelation)) {
$this->storeCentralPurchasingRelations->removeElement($storeCentralPurchasingRelation);
// set the owning side to null (unless already changed)
if ($storeCentralPurchasingRelation->getStore() === $this) {
$storeCentralPurchasingRelation->setStore(null);
}
}
return $this;
}
public function getFreelancerAssistant(): ?FreelancerAssistant
{
return $this->freelancerAssistant;
}
public function setFreelancerAssistant(?FreelancerAssistant $freelancerAssistant): self
{
$this->freelancerAssistant = $freelancerAssistant;
return $this;
}
public function hasStorePage(): bool
{
return !is_null($this->storePage);
}
public function getStorePage(): ?StorePage
{
return $this->storePage;
}
public function setStorePage(?StorePage $storePage): self
{
$this->storePage = $storePage;
return $this;
}
public function hasPagePremium(): bool
{
return !is_null($this->storePage);
}
public function getSlug(): ?string
{
return $this->slug;
}
public function setSlug(?string $slug): self
{
$this->slug = $slug;
return $this;
}
public function getTeaser(): ?string
{
return $this->teaser;
}
public function setTeaser(?string $teaser): self
{
$this->teaser = $teaser;
return $this;
}
/**
* Return name and city.
* If city as area, the number of the area is displayed. ex: AUDIKA (13006)
* Lyon (690xx) Marseille (130xx) and Paris (750xx)
* @return string
*/
public function getNameAndCity()
{
$areaString = '';
$cityString = iconv("UTF-8", "UTF-8//IGNORE", $this->getCity());
preg_match('/^(?:13|75|69)0(\d{2})$/', $this->getZipCode(), $areaNumberResult);
if (count($areaNumberResult) > 0) {
$areaString = ' ' . $areaNumberResult[1];
}
return $this->name . ' (' . $cityString . $areaString . ')';
}
public function getHandicapAccess(): ?bool
{
return $this->handicapAccess;
}
public function setHandicapAccess(?bool $handicapAccess): self
{
$this->handicapAccess = $handicapAccess;
return $this;
}
public function getPaymentMethod(): ?PaymentMethod
{
return $this->paymentMethod;
}
public function setPaymentMethod(?PaymentMethod $paymentMethod): self
{
$this->paymentMethod = $paymentMethod;
return $this;
}
public function isQualificationEnabled(): ?bool
{
return $this->qualificationEnabled;
}
public function setQualificationEnabled(bool $qualificationEnabled): self
{
$this->qualificationEnabled = $qualificationEnabled;
return $this;
}
/**
* @return Collection<int, StoreIndexation>
*/
public function getStoreIndexations(): Collection
{
return $this->storeIndexations;
}
/**
* Return current store indexation
* @return StoreIndexation|null
*/
public function getCurrentStoreIndexation(): ?StoreIndexation
{
$now = new DateTime();
$firstIndexation = $this->storeIndexations->filter(function (StoreIndexation $storeIndexation) use ($now) {
return $storeIndexation->getStartAt() <= $now && $storeIndexation->getEndAt() >= $now;
})->first();
return $firstIndexation ? $firstIndexation : null;
}
public function hasCurrentStoreIndexation(): bool
{
return !is_null($this->getCurrentStoreIndexation());
}
/**
* Return not expired store indexation
* @return ArrayCollection
*/
public function getNotExpiredStoreIndexations(): ArrayCollection
{
$now = new DateTime();
return $this->storeIndexations->filter(function (StoreIndexation $storeIndexation) use ($now) {
return $storeIndexation->getEndAt() >= $now;
});
}
public function getLastNotExpiredStoreIndexation(): ?StoreIndexation
{
$notExpiredStoreIndexations = $this->getNotExpiredStoreIndexations();
$last = $notExpiredStoreIndexations->last();
return $last ? $last : null;
}
public function isIndexableFromBusinessModel(): bool
{
$admin = $this->getAdministrator();
//only true for partner and freelance who had credit when we changed the business model (v2)
return $admin && ($admin->isPartner() || ($admin->isFreelancer() && $admin->hasAllStoresIndexableTemporary()));
}
public function isIndexable(): bool
{
//TODO: quand plus aucun indé n'aura de business model "crédit" sans centre indexé, le 1er test pourra être remplacé par "isPartner()"
return $this->isIndexableFromBusinessModel() ||
$this->hasCurrentStoreIndexation();
}
public function addStoreIndexation(StoreIndexation $storeIndexation): self
{
if (!$this->storeIndexations->contains($storeIndexation)) {
$this->storeIndexations[] = $storeIndexation;
$storeIndexation->setStore($this);
}
return $this;
}
public function removeStoreIndexation(StoreIndexation $storeIndexation): self
{
if ($this->storeIndexations->removeElement($storeIndexation)) {
// set the owning side to null (unless already changed)
if ($storeIndexation->getStore() === $this) {
$storeIndexation->setStore(null);
}
}
return $this;
}
public function getStorePlaceData(): ?StorePlaceData
{
return $this->storePlaceData;
}
public function setStorePlaceData(?StorePlaceData $storePlaceData): self
{
// unset the owning side of the relation if necessary
if ($storePlaceData === null && $this->storePlaceData !== null) {
$this->storePlaceData->setStore(null);
}
// set the owning side of the relation if necessary
if ($storePlaceData !== null && $storePlaceData->getStore() !== $this) {
$storePlaceData->setStore($this);
}
$this->storePlaceData = $storePlaceData;
return $this;
}
}