<?php
namespace App\Entity;
use App\Entity\Enum\BusinessModelEnum;
use App\Entity\Enum\FreelancerBusinessSoftwareEnum;
use App\Entity\Enum\OnboardingStatusEnum;
use App\Entity\Traits\CreditOwnerInterface;
use App\Entity\Traits\CreditOwnerTrait;
use DateTime;
use DateTimeInterface;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\HttpFoundation\File\File;
use Symfony\Component\Validator\Constraints as Assert;
use Vich\UploaderBundle\Mapping\Annotation as Vich;
use Gedmo\Mapping\Annotation as Gedmo;
/**
* @ORM\Entity(repositoryClass="App\Repository\FreelancerRepository")
* @ORM\EntityListeners({"App\Listener\FreelancerListener"})
* @Gedmo\Loggable(logEntryClass="App\Entity\LogEntry")
* @Vich\Uploadable
*/
class Freelancer extends Administrator implements CreditOwnerInterface
{
use CreditOwnerTrait;
public const CREDIT_ALMOST_DOWN = 2;
private const PAYASYOUGO_CAPPING_DEFAULT = 5;
/**
* Fake deadline used to identify freelance who have their store indexable till they reach 0 credit
*/
public const BUSINESS_MODEL_FAKE_DEADLINE = '2030-01-01 00:00:00';
/**
* Deadline relacing the 2 months previously used before passing businessModel from 'credit' to 'none'
*/
public const BUSINESS_FORCED_DEADLINE = '2025-05-20 00:00:00';
/**
* @ORM\Column(type="string", length=25, nullable=true)
*/
private $vatNumber;
/**
* @Assert\NotBlank()
* @Assert\Regex(pattern="/^\d{14}$/", message="Le numéro de SIRET n'est pas valide.")
* @ORM\Column(type="string", length=25)
*/
private $siretNumber;
/**
* @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 $phoneNumberSmsNotification;
/**
* @ORM\Column(type="string", length=255, nullable=true)
*/
private $emailNotification;
/**
* @var boolean
* @ORM\Column(type="boolean")
*/
private $isSmsNotification;
/**
* @var boolean
* @ORM\Column(type="boolean")
*/
private $isEmailNotification;
/**
* specify if this freelancer has already been an premium member (even if nbCredit is null)
* @var boolean
* @ORM\Column(type="boolean", options={"default" : false})
*/
private $hasBeenPremium;
/**
* @ORM\Column(type="datetime", nullable=true)
*/
private $soldOutAt;
/**
* specify if this freelancer is active and can receive prospect
* @var boolean
* @ORM\Column(type="boolean", options={"default" : true})
*/
private $isEnabled = 1;
/**
* @ORM\OneToMany(targetEntity="App\Entity\Order", mappedBy="freelancer", cascade={"remove"})
*/
private $orders;
/**
* @ORM\OneToMany(targetEntity=FreelancerAssistant::class, mappedBy="freelancer")
*/
private $freelancerAssistants;
/**
* @ORM\Column(type="datetime", nullable=true)
*/
private $updatedAt;
/**
* @ORM\Column(type="string", length=255, nullable=true)
*/
private $picturePath;
/**
* @assert\File( mimeTypes = {"image/jpeg", "image/png", "image/gif", "image/jpg","image/webp"})
* @Vich\UploadableField(mapping="freelancer_images", fileNameProperty="picturePath")
* @var File
*/
private $pictureFile;
/**
* @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="freelancer_logo", fileNameProperty="logoPath")
* @var File
*/
private $logoFile;
/**
* @ORM\Column(type="integer")
*/
private $nbPendingLead;
/**
* @ORM\Column(type="integer", nullable=true)
*/
private $nbCreditSms;
/**
* @ORM\OneToMany(targetEntity=FreelancerComment::class, mappedBy="freelancer", orphanRemoval=true)
*/
private $comments;
/**
* @ORM\Column(type="integer", nullable=true)
*/
private $businessSoftware;
/**
* @ORM\Column(type="integer", nullable=true)
* @Assert\Range(
* min = 1,
* max = 99,
* notInRangeMessage = "Le nombre de contact doit être entre {{ min }} et {{ max }}",
* )
*/
private ?int $payAsYouGoCapping = self::PAYASYOUGO_CAPPING_DEFAULT;
/**
* @ORM\OneToMany(targetEntity=CreditCost::class, mappedBy="administrator")
*/
private Collection $creditCosts;
/**
* @ORM\Column(type="string", length=255, nullable=true)
*/
private string $onboardingStatus = OnboardingStatusEnum::IN_PROGRESS;
/**
* @ORM\Column(type="integer", name="stat_nb_credit_purchased")
*/
private int $nbCreditPurchased = 0;
/**
* @ORM\Column(type="integer", name="stat_nb_lead_delivered")
*/
private int $nbLeadDelivered = 0;
/**
* @ORM\Column(type="integer", name="stat_nb_sales")
*/
private int $nbSales = 0;
public function __construct()
{
parent::__construct();
$this->isEmailNotification = true;
$this->isSmsNotification = true;
$this->hasBeenPremium = false;
$this->orders = new ArrayCollection();
$this->freelancerAssistants = new ArrayCollection();
$this->comments = new ArrayCollection();
$this->setBusinessModel(BusinessModelEnum::NONE);
}
/**
* @return mixed
*/
public function getUpdatedAt(): ?DateTimeInterface
{
return $this->updatedAt;
}
/**
* @param mixed $updatedAt
*/
public function setUpdatedAt(?DateTimeInterface $updatedAt): self
{
$this->updatedAt = $updatedAt;
return $this;
}
public function getPicturePath(): ?string
{
return $this->picturePath;
}
public function setPicturePath(?string $picturePath)
{
$this->picturePath = $picturePath;
return $this;
}
public function hasPicture()
{
return !is_null($this->picturePath);
}
public function setPictureFile(File $picturePath = null)
{
$this->pictureFile = $picturePath;
if ($picturePath) {
$this->updatedAt = new DateTime('now');
}
}
public function getPictureFile()
{
return $this->pictureFile;
}
/**
* Représente l'entité par son nom et son type (admin ou freelance)
* @return string
*/
public function getLabelWithType()
{
return $this->getName() . ' [Indépendant]';
}
public function getNameAndEmailUser()
{
if ($this->getUser() !== null) {
return strtoupper($this->getName()) . ' [' . $this->getUser()->getEmail() . ']';
} else {
return $this->getName();
}
}
public function getId(): ?int
{
return $this->id;
}
public function getVatNumber(): ?string
{
return $this->vatNumber;
}
public function setVatNumber(string $vatNumber): self
{
$this->vatNumber = $vatNumber;
return $this;
}
public function setNbCredit(int $nbCredit): self
{
$this->nbCredit = $nbCredit;
$this->updateCustomerStatus();
return $this;
}
public function setNbPendingLead(int $nbPendingLead): self
{
$this->nbPendingLead = $nbPendingLead;
$this->updateCustomerStatus();
return $this;
}
/**
* when nbCredit or nbPendingLead is updated, we check if this freelancer is still a customer
* - freelance must be enabled
* AND
* - freelance must have enough credit to receive leads
* OR
* - freelance must have opted-in for "pay as you go" credit && have less than 2 pending credit costs
*/
public function updateCustomerStatus()
{
if ($this->isEnabled() && (($this->nbCredit - $this->nbPendingLead) > 0 || ($this->isPayAsYouGo() && !$this->isPayAsYouGoThresholdReached()))) {
$this->setIsCustomer(true);
} else {
$this->setIsCustomer(false);
}
}
public function getNbPendingLead(): ?int
{
return $this->nbPendingLead;
}
public function getSiretNumber(): ?string
{
return $this->siretNumber;
}
public function setSiretNumber(string $siretNumber): self
{
$siretNumber = str_replace(' ', '', $siretNumber);
$this->siretNumber = $siretNumber;
return $this;
}
public function getPhoneNumberSmsNotification(): ?string
{
return $this->phoneNumberSmsNotification;
}
public function setPhoneNumberSmsNotification(?string $phoneNumberSmsNotification): self
{
$this->phoneNumberSmsNotification = $phoneNumberSmsNotification;
return $this;
}
public function mustBeNotifiedBySms(): ?bool
{
return $this->getIsSmsNotification() && $this->getPhoneNumberSmsNotification();
}
/**
* @return string
* @deprecated use Formatter->phoneNumberInternationalFormat instead
*/
public function getPhoneNumberSmsNotificationFormatted()
{
return wordwrap($this->getPhoneNumberSmsNotification(), 2, ' ', true);
}
public function getEmailNotification(): ?string
{
return $this->emailNotification;
}
public function setEmailNotification(?string $emailNotification): self
{
$this->emailNotification = $emailNotification;
return $this;
}
public function mustBeNotifiedByEmail(): ?bool
{
return $this->getIsEmailNotification() && !empty($this->getEmailNotification());
}
// Returns an array with every mail addresses for a store
public function getEmailList(): array
{
// Optim: use constant for delimiter?
return explode(";", $this->getEmailNotification());
}
public function getIsSmsNotification(): ?bool
{
return $this->isSmsNotification;
}
public function setIsSmsNotification(bool $isSmsNotification): self
{
$this->isSmsNotification = $isSmsNotification;
return $this;
}
public function getIsEmailNotification(): ?bool
{
return $this->isEmailNotification;
}
public function setIsEmailNotification(bool $isEmailNotification): self
{
$this->isEmailNotification = $isEmailNotification;
return $this;
}
/**
* @return bool
*/
public function isHasBeenPremium(): bool
{
return $this->hasBeenPremium;
}
/**
* @param bool $hasBeenPremium
*/
public function setHasBeenPremium(bool $hasBeenPremium): void
{
$this->hasBeenPremium = $hasBeenPremium;
}
/**
* @return Collection|Order[]
*/
public function getOrders(): Collection
{
return $this->orders;
}
public function addOrder(Order $order): self
{
if (!$this->orders->contains($order)) {
$this->orders[] = $order;
$order->setFreelancer($this);
}
return $this;
}
public function removeOrder(Order $order): self
{
if ($this->orders->contains($order)) {
$this->orders->removeElement($order);
// set the owning side to null (unless already changed)
if ($order->getFreelancer() === $this) {
$order->setFreelancer(null);
}
}
return $this;
}
/**
* @return bool
*/
public function isEnabled(): bool
{
return $this->isEnabled;
}
/**
* @param bool $isEnabled
*/
public function setIsEnabled(bool $isEnabled): void
{
$this->isEnabled = $isEnabled;
$this->updateCustomerStatus();
}
public function getHasBeenPremium(): ?bool
{
return $this->hasBeenPremium;
}
public function getIsEnabled(): ?bool
{
return $this->isEnabled;
}
/**
* @return Collection|FreelancerAssistant[]
*/
public function getFreelancerAssistants(): Collection
{
return $this->freelancerAssistants;
}
public function addFreelancerAssistant(FreelancerAssistant $freelancerAssistant): self
{
if (!$this->freelancerAssistants->contains($freelancerAssistant)) {
$this->freelancerAssistants[] = $freelancerAssistant;
$freelancerAssistant->setFreelancer($this);
}
return $this;
}
public function removeFreelancerAssistant(FreelancerAssistant $freelancerAssistant): self
{
if ($this->freelancerAssistants->contains($freelancerAssistant)) {
$this->freelancerAssistants->removeElement($freelancerAssistant);
// set the owning side to null (unless already changed)
if ($freelancerAssistant->getFreelancer() === $this) {
$freelancerAssistant->setFreelancer(null);
}
}
return $this;
}
public function getSoldOutAt(): ?DateTimeInterface
{
return $this->soldOutAt;
}
public function setSoldOutAt(?DateTimeInterface $soldOutAt): self
{
$this->soldOutAt = $soldOutAt;
return $this;
}
public function getNbCreditSms(): ?int
{
return $this->nbCreditSms;
}
public function setNbCreditSms(?int $nbCreditSms): self
{
$this->nbCreditSms = $nbCreditSms;
return $this;
}
/**
* @return Collection|FreelancerComment[]
*/
public function getComments(): Collection
{
return $this->comments;
}
public function addComment(FreelancerComment $comment): self
{
if (!$this->comments->contains($comment)) {
$this->comments[] = $comment;
$comment->setFreelancer($this);
}
return $this;
}
public function removeComment(FreelancerComment $comment): self
{
if ($this->comments->contains($comment)) {
$this->comments->removeElement($comment);
// set the owning side to null (unless already changed)
if ($comment->getFreelancer() === $this) {
$comment->setFreelancer(null);
}
}
return $this;
}
public function getLastCommentExtract(): ?string
{
return !$this->comments->isEmpty() ? substr($this->comments->last()->getComment(), 0, 30) : null;
}
public function getLogoPath(): ?string
{
return $this->logoPath;
}
public function setLogoPath(?string $logoPath): self
{
$this->logoPath = $logoPath;
return $this;
}
public function getBusinessSoftware(): ?int
{
return $this->businessSoftware;
}
public function setBusinessSoftware(?int $businessSoftware): self
{
$this->businessSoftware = $businessSoftware;
return $this;
}
public function getBusinessSoftwareName(): string
{
if (is_null($this->getBusinessSoftware())) {
return 'Aucune';
}
return FreelancerBusinessSoftwareEnum::getItemName($this->getBusinessSoftware());
}
public function setLogoFile(File $logoPath = null)
{
$this->logoFile = $logoPath;
if ($logoPath) {
$this->updatedAt = new DateTime('now');
}
}
public function getLogoFile()
{
return $this->logoFile;
}
public function setBusinessModel(int $businessModel): void
{
parent::setBusinessModel($businessModel);
$this->updateCustomerStatus();
}
public function hasAllStoresIndexableTemporary(): bool
{
//if a deadline is set, stores remain indexable
return !is_null($this->businessModelDeadline);// && $this->businessModelDeadline->format('Y-m-d H:i:s') == self::BUSINESS_MODEL_FAKE_DEADLINE;
}
public function markAsAllStoresIndexableTemporary(): void
{
$this->setBusinessModelDeadline(new DateTime(self::BUSINESS_MODEL_FAKE_DEADLINE));
}
public function getCreditCosts(): Collection
{
return $this->creditCosts;
}
public function addCreditCost(CreditCost $creditCost): self
{
if (!$this->creditCosts->contains($creditCost)) {
$this->creditCosts->add($creditCost);
$creditCost->setAdministrator($this);
}
return $this;
}
public function getPendingCreditCosts(): Collection
{
return $this->creditCosts->filter(function (CreditCost $creditCost) {
return $creditCost->isPending();
});
}
public function getNbPendingCreditCosts(): int
{
return $this->getPendingCreditCosts()->count();
}
public function isPayAsYouGoThresholdReached(): bool
{
if (is_null($this->getPayAsYouGoCapping())) {
return false;
}
return $this->getNbPendingCreditCosts() + $this->getNbPendingLead() >= $this->getPayAsYouGoCapping();
}
public function setCreditCosts(Collection $creditCosts): void
{
$this->creditCosts = $creditCosts;
}
public function getPayAsYouGoCapping(): ?int
{
if (is_null($this->payAsYouGoCapping)) {
return self::PAYASYOUGO_CAPPING_DEFAULT;
}
return $this->payAsYouGoCapping;
}
public function setPayAsYouGoCapping(?int $payAsYouGoCapping): void
{
$this->payAsYouGoCapping = $payAsYouGoCapping;
$this->updateCustomerStatus();
}
public function getOnboardingStatus(): string
{
return $this->onboardingStatus;
}
public function setOnboardingStatus(string $onboardingStatus): void
{
$this->onboardingStatus = $onboardingStatus;
}
public function getNbCreditPurchased(): int
{
return $this->nbCreditPurchased;
}
public function setNbCreditPurchased(int $nbCreditPurchased): void
{
$this->nbCreditPurchased = $nbCreditPurchased;
}
public function getNbLeadDelivered(): int
{
return $this->nbLeadDelivered;
}
public function setNbLeadDelivered(int $nbLeadDelivered): void
{
$this->nbLeadDelivered = $nbLeadDelivered;
}
public function getNbSales(): int
{
return $this->nbSales;
}
public function setNbSales(int $nbSales): void
{
$this->nbSales = $nbSales;
}
/**
*/
public function hasAtLeastOneIndexableStore(): bool
{
return $this->getStores()->exists(function ($key, $store) {
return $store->hasCurrentStoreIndexation();
});
}
public function getAgeCapping(): ?int
{
return 50;
}
}