<?php
namespace App\Entity\OnlineShop;
use App\Annotation\SiteAware;
use App\Entity\AbstractBase;
use App\Entity\Interfaces\DocumentInterface;
use App\Entity\Interfaces\ImageInterface;
use App\Entity\Interfaces\PublishedInterface;
use App\Entity\Interfaces\SiteInterface;
use App\Entity\MiniAbstractBase;
use App\Entity\Traits\DescriptionTrait;
use App\Entity\Traits\HasDocumentTrait;
use App\Entity\Traits\HasImageTrait;
use App\Entity\Traits\NullableNameTrait;
use App\Entity\Traits\OnlyAdminTrait;
use App\Entity\Traits\PublishedTrait;
use App\Entity\Traits\SiteTrait;
use App\Entity\User;
use App\Enum\UserRolesEnum;
use App\Persistence\Model\UserGroupAccessType;
use App\Repository\OnlineShop\ProductRepository;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;
use Doctrine\ORM\Mapping as ORM;
use Gedmo\Mapping\Annotation as Gedmo;
use Money\Currency;
use Money\Money;
use Symfony\Component\HttpFoundation\File\File;
use Symfony\Component\Security\Core\User\UserInterface;
use Symfony\Component\Validator\Constraints as Assert;
use Tbbc\MoneyBundle\Formatter\MoneyFormatter;
use Vich\UploaderBundle\Mapping\Annotation as Vich;
/**
* @ORM\Table(name="vulco_product", indexes={@ORM\Index(name="product_site_idx", columns={"site"})}, uniqueConstraints={@ORM\UniqueConstraint(name="familiy_and_supplier_reference_unique_idx", columns={"family_id", "supplier_reference", "removed_at"})})})
*
* @ORM\Entity(repositoryClass=ProductRepository::class)
*
* @Gedmo\SoftDeleteable(fieldName="removedAt", timeAware=false)
*
* @SiteAware(siteFieldName="site")
*
* @Vich\Uploadable
*/
class Product extends AbstractBase implements SiteInterface, DocumentInterface, PublishedInterface, ImageInterface
{
use DescriptionTrait;
use HasDocumentTrait;
use HasImageTrait;
use NullableNameTrait;
use OnlyAdminTrait;
use PublishedTrait;
use SiteTrait;
public const RATE_PRICE = 1;
public const COST_PRICE = 2;
/**
* @ORM\Column(type="string", length=200, nullable=false)
*/
private string $reference;
/**
* @ORM\Column(type="string", length=40, nullable=false)
*/
private ?string $name = null;
/**
* @ORM\Column(type="text", length=10000, nullable=true)
*/
private ?string $description = null;
/**
* @ORM\Column(type="integer")
*/
private int $ratePriceAmount;
/**
* @ORM\Column(type="string", length=64, nullable=false)
*/
private string $ratePriceCurrency;
/**
* @ORM\Column(type="money")
*/
private Money $costPrice;
/**
* @ORM\Column(type="money", nullable=true)
* At the moment we only use this field for orders export of garages with GRIPS software
*/
private ?Money $pvpPrice = null;
/**
* @ORM\Column(type="money", nullable=true)
* Sale price (precio de oferta). At the moment we only use it for the Meta catalog feed. (sale_price field)
*/
private ?Money $salePrice = null;
/**
* @ORM\Column(type="string", length=255, nullable=true)
* To assimilate families with GRIPS software
*/
private ?string $positionGroup = null;
/**
* @ORM\Column(type="money")
*/
private Money $sigaus;
/**
* @ORM\Column(type="money")
*/
private Money $greenPoint;
/**
* @ORM\Column(type="money")
*/
private Money $ecoTax;
/**
* @ORM\Column(type="decimal", precision=10, scale=2)
*/
private float $weight;
/**
* @ORM\Column(type="integer")
*/
private int $transportPoints;
/**
* @ORM\Column(type="text", length=10000, nullable=true)
*/
private ?string $promotion = null;
/**
* @ORM\Column(type="text", length=10000, nullable=true)
*/
private ?string $imageUrl = null;
/**
* @ORM\Column(type="string", length=255, nullable=true)
*/
private ?string $supplierReference = null;
/**
* @ORM\Column(type="decimal", precision=10, scale=2)
*/
private float $volume;
/**
* @ORM\Column(type="integer", nullable=true)
*/
private ?int $units = null;
/**
* @Vich\UploadableField(mapping="product_image", fileNameProperty="imageName")
*/
private ?File $image = null;
/**
* @ORM\Column(type="string", length=255, nullable=true)
*/
private ?string $imageName = null;
/**
* @Vich\UploadableField(mapping="product_document", fileNameProperty="documentName")
*/
private ?File $document = null;
/**
* @ORM\Column(type="string", length=255, nullable=true)
*
* @Vich\UploadableField(mapping="document", fileNameProperty="imageName")
*/
private ?string $documentName = null;
/**
* @ORM\Column(type="string", length=255, nullable=true)
*/
private ?string $documentRealName = null;
/**
* @ORM\Column(type="boolean")
*/
private bool $noLinealDiscountEnabled;
/**
* @ORM\Column(type="boolean")
*/
private bool $onlyAdminVisible;
/**
* @ORM\Column(type="boolean", nullable=false, options={"default": 0})
*/
private bool $featured = false;
/**
* @ORM\Column(type="boolean", nullable=false, options={"default": 0})
* Use it to include the product in the Meta catalog feed
*/
private bool $metaCatalog = false;
/**
* @ORM\ManyToOne(targetEntity="App\Entity\OnlineShop\Family", fetch="EAGER")
*
* @ORM\JoinColumn(name="family_id", referencedColumnName="id")
*/
private Family $family;
/**
* @ORM\ManyToMany(targetEntity="App\Entity\OnlineShop\ScalePrice", cascade={"persist"}, orphanRemoval=true)
*
* @ORM\JoinTable(name="vulco_shop_product_scale_price",
* joinColumns={@ORM\JoinColumn(name="product_id", referencedColumnName="id", onDelete="CASCADE")},
* inverseJoinColumns={@ORM\JoinColumn(name="scale_price_id", referencedColumnName="id", onDelete="CASCADE", unique=true)}
* )
*
* @ORM\OrderBy({"quantity": "ASC"})
*/
private ?Collection $scalePrices;
/**
* @ORM\ManyToMany(targetEntity="App\Entity\OnlineShop\VolumeDiscount", cascade={"persist"}, orphanRemoval=true)
*
* @ORM\JoinTable(name="vulco_shop_product_volume_discount",
* joinColumns={@ORM\JoinColumn(name="product_id", referencedColumnName="id", onDelete="CASCADE")},
* inverseJoinColumns={@ORM\JoinColumn(name="volume_discount_id", referencedColumnName="id", onDelete="CASCADE", unique=true)}
* )
*
* @ORM\OrderBy({"volume": "ASC"})
*/
private ?Collection $volumeDiscounts;
/**
* @ORM\OneToMany(targetEntity="App\Entity\OnlineShop\ProductSpecialPrice", mappedBy="product", cascade={"persist", "remove"}, orphanRemoval=true)
*
* @Assert\Valid()
*/
private ?Collection $productSpecialPrices;
public function __construct()
{
$this->scalePrices = new ArrayCollection();
$this->volumeDiscounts = new ArrayCollection();
$this->productSpecialPrices = new ArrayCollection();
$this->noLinealDiscountEnabled = false;
$this->onlyAdminVisible = false;
}
public function getReference(): string
{
return $this->reference;
}
public function setReference(string $reference): self
{
$this->reference = $reference;
return $this;
}
public function getRatePrice(): ?Money
{
if (!$this->ratePriceCurrency) {
return null;
}
if (!$this->ratePriceAmount) {
return new Money(0, new Currency($this->ratePriceCurrency));
}
return new Money($this->ratePriceAmount, new Currency($this->ratePriceCurrency));
}
public function getRatePriceWithDiscount($quantity): Money
{
return $this->getPriceWithDiscount($this->getRatePrice(), $quantity, self::RATE_PRICE);
}
public function getRatePriceWithDiscountAndTax($quantity): Money
{
$ratePrice = $this->getRatePriceWithDiscount($quantity);
return $ratePrice->add($this->sigaus)->add($this->greenPoint)->add($this->ecoTax);
}
public function setRatePrice(Money $ratePrice): self
{
$this->ratePriceAmount = $ratePrice->getAmount();
$this->ratePriceCurrency = $ratePrice->getCurrency()->getCode();
return $this;
}
public function getRatePriceAmount(): int
{
return $this->ratePriceAmount;
}
public function setRatePriceAmount(int $ratePriceAmount): self
{
$this->ratePriceAmount = $ratePriceAmount;
return $this;
}
public function getRatePriceCurrency(): string
{
return $this->ratePriceCurrency;
}
public function setRatePriceCurrency(string $ratePriceCurrency): self
{
$this->ratePriceCurrency = $ratePriceCurrency;
return $this;
}
public function getSpecialPriceActive(): ?ProductSpecialPrice
{
if ($this->hasSpecialPriceActive()) {
/** @var ProductSpecialPrice $productSpecialPrice */
foreach ($this->getProductSpecialPrices() as $productSpecialPrice) {
if ($productSpecialPrice->isActive()) {
return $productSpecialPrice;
}
}
}
return null;
}
public function hasSpecialPriceActive(): bool
{
if ($this->getProductSpecialPrices() && !$this->getProductSpecialPrices()->isEmpty()) {
/** @var ProductSpecialPrice $productSpecialPrice */
foreach ($this->getProductSpecialPrices() as $productSpecialPrice) {
if ($productSpecialPrice->isActive()) {
return true;
}
}
}
return false;
}
public function getCostPrice(): Money
{
return $this->costPrice;
}
public function getCostPriceWithDiscount($quantity): Money
{
return $this->getPriceWithDiscount($this->getCostPrice(), $quantity, self::COST_PRICE);
}
public function setCostPrice(Money $costPrice): self
{
$this->costPrice = $costPrice;
return $this;
}
public function getPvpPrice(): ?Money
{
return $this->pvpPrice;
}
public function setPvpPrice(?Money $pvpPrice): self
{
$this->pvpPrice = $pvpPrice;
return $this;
}
/**
* Sale price (precio de oferta). At the moment we only use it for the Meta catalog feed. (sale_price field)
* @return Money|null
*/
public function getSalePrice(): ?Money
{
return $this->salePrice;
}
public function getSalePriceAsString(): ?string
{
if (!empty($this->salePrice)) {
// @see \Tbbc\MoneyBundle\Formatter\MoneyFormatter::asFloat
$amount = (float) $this->salePrice->getAmount();
$amount = $amount / pow(10, 2);
return MiniAbstractBase::getFloatAsString($amount);
}
return null;
}
public function setSalePrice(?Money $salePrice): self
{
$this->salePrice = $salePrice;
return $this;
}
public function getPositionGroup(): ?string
{
return $this->positionGroup;
}
public function setPositionGroup(?string $positionGroup): self
{
$this->positionGroup = $positionGroup;
return $this;
}
public function getPriceWithDiscount(Money $price, $quantity, $type): Money
{
if ($this->getScalePrices() && !$this->getScalePrices()->isEmpty()) {
return $this->calculatePriceByScalePrice($price, $quantity, $type);
}
if ($this->getVolumeDiscounts() && !$this->getVolumeDiscounts()->isEmpty()) {
return $this->calculatePriceByVolumeDiscount($price, $quantity, $type);
}
return $price;
}
public function getSigaus(): Money
{
return $this->sigaus;
}
public function setSigaus(Money $sigaus): self
{
$this->sigaus = $sigaus;
return $this;
}
public function getGreenPoint(): Money
{
return $this->greenPoint;
}
public function setGreenPoint(Money $greenPoint): self
{
$this->greenPoint = $greenPoint;
return $this;
}
public function getEcoTax(): Money
{
return $this->ecoTax;
}
public function setEcoTax(Money $ecoTax): self
{
$this->ecoTax = $ecoTax;
return $this;
}
public function getWeight(): float
{
return $this->weight;
}
public function setWeight(float $weight): self
{
$this->weight = $weight;
return $this;
}
public function getTransportPoints(): int
{
return $this->transportPoints;
}
public function setTransportPoints(int $transportPoints): self
{
$this->transportPoints = $transportPoints;
return $this;
}
public function getPromotion(): ?string
{
return $this->promotion;
}
public function setPromotion(?string $promotion): self
{
$this->promotion = $promotion;
return $this;
}
public function getImageUrl(): ?string
{
return $this->imageUrl;
}
public function setImageUrl(?string $imageUrl): self
{
$this->imageUrl = $imageUrl;
return $this;
}
public function getSupplierReference(): ?string
{
return $this->supplierReference;
}
public function setSupplierReference(?string $supplierReference): self
{
$this->supplierReference = $supplierReference;
return $this;
}
public function getVolume(): float
{
return $this->volume;
}
public function setVolume(float $volume): self
{
$this->volume = $volume;
return $this;
}
public function getUnits(): ?int
{
return $this->units;
}
public function setUnits(?int $units): self
{
$this->units = $units;
return $this;
}
public function getImageName(): ?string
{
return $this->imageName;
}
public function setImageName(?string $imageName): self
{
$this->imageName = $imageName;
return $this;
}
public function getDocumentName(): ?string
{
return $this->documentName;
}
public function setDocumentName(?string $documentName): self
{
$this->documentName = $documentName;
return $this;
}
public function getDocumentRealName(): ?string
{
return $this->documentRealName;
}
public function setDocumentRealName(?string $documentRealName): self
{
$this->documentRealName = $documentRealName;
return $this;
}
public function isNoLinealDiscountEnabled(): bool
{
return $this->noLinealDiscountEnabled;
}
public function setNoLinealDiscountEnabled(bool $noLinealDiscountEnabled): self
{
$this->noLinealDiscountEnabled = $noLinealDiscountEnabled;
return $this;
}
public function isFeatured(): bool
{
return $this->featured;
}
public function setFeatured(bool $featured): self
{
$this->featured = $featured;
return $this;
}
public function isMetaCatalog(): bool
{
return $this->metaCatalog;
}
public function setMetaCatalog(bool $metaCatalog): self
{
$this->metaCatalog = $metaCatalog;
return $this;
}
public function getFamily(): Family
{
return $this->family;
}
public function setFamily(Family $family): self
{
$this->family = $family;
return $this;
}
public function getScalePrices(): ?Collection
{
return $this->scalePrices;
}
public function setScalePrices(?Collection $scalePrices): self
{
$this->scalePrices = $scalePrices;
return $this;
}
public function addScalePrice(ScalePrice $scalePrice): self
{
if (!$this->scalePrices->contains($scalePrice)) {
$this->scalePrices->add($scalePrice);
}
return $this;
}
public function removeScalePrice(ScalePrice $scalePrice): self
{
if ($this->scalePrices->contains($scalePrice)) {
$this->scalePrices->removeElement($scalePrice);
}
return $this;
}
public function deleteScalePrice(ScalePrice $scalePrice): self
{
return $this->removeScalePrice($scalePrice);
}
public function getVolumeDiscounts(): ?Collection
{
return $this->volumeDiscounts;
}
public function setVolumeDiscounts(?Collection $volumeDiscounts): self
{
$this->volumeDiscounts = $volumeDiscounts;
return $this;
}
public function addVolumeDiscount(VolumeDiscount $volumeDiscount): self
{
if (!$this->volumeDiscounts->contains($volumeDiscount)) {
$this->volumeDiscounts->add($volumeDiscount);
}
return $this;
}
public function removeVolumeDiscount(VolumeDiscount $volumeDiscount): self
{
if ($this->volumeDiscounts->contains($volumeDiscount)) {
$this->volumeDiscounts->removeElement($volumeDiscount);
}
return $this;
}
public function deleteVolumeDiscount(VolumeDiscount $volumeDiscount): self
{
return $this->removeVolumeDiscount($volumeDiscount);
}
public function getProductSpecialPrices(): ?Collection
{
return $this->productSpecialPrices;
}
public function setProductSpecialPrices(?Collection $productSpecialPrices): self
{
$this->productSpecialPrices = $productSpecialPrices;
return $this;
}
public function addProductSpecialPrice(ProductSpecialPrice $productSpecialPrice): self
{
if (!$this->productSpecialPrices->contains($productSpecialPrice)) {
$productSpecialPrice->setProduct($this);
$this->productSpecialPrices->add($productSpecialPrice);
}
return $this;
}
public function removeProductSpecialPrice(ProductSpecialPrice $productSpecialPrice): self
{
if ($this->productSpecialPrices->contains($productSpecialPrice)) {
$this->productSpecialPrices->removeElement($productSpecialPrice);
}
return $this;
}
public function deleteProductSpecialPrice(ProductSpecialPrice $productSpecialPrice): self
{
return $this->removeProductSpecialPrice($productSpecialPrice);
}
public function calculatePriceByVolumeDiscount(Money $price, $quantity, $type): Money
{
$volumeDiscounts = $this->getVolumeDiscounts();
$highVolumeDiscount = null;
/** @var VolumeDiscount $volumeDiscount */
foreach ($volumeDiscounts as $volumeDiscount) {
$quantityThreshold = $volumeDiscount->getVolume();
if ($quantity >= $quantityThreshold) {
$highVolumeDiscount = $volumeDiscount;
}
}
if ($highVolumeDiscount instanceof VolumeDiscount) {
$discount = self::COST_PRICE === $type ? $highVolumeDiscount->getCostDiscount() : $highVolumeDiscount->getRateDiscount();
return $price->multiply(1 - $discount / 100);
}
return $price;
}
public function calculatePriceByScalePrice(Money $price, $quantity, $type): Money
{
// because if isNoLinealDiscountEnabled this lineals discounts are for no lineal discount and are calculated as a disccount in cartToOrderTransformer
if (!$this->isNoLinealDiscountEnabled()) {
$scalePrices = $this->getScalePrices();
foreach ($scalePrices as $scalePrice) {
$quantityThreshold = $scalePrice->getQuantity();
if ($quantity >= $quantityThreshold) {
$price = self::COST_PRICE === $type ? $scalePrice->getCostPrice() : $scalePrice->getRatePrice();
}
}
}
return $price;
}
public function userHasAccess(UserInterface $user): bool
{
/** @var User $user */
$family = $this->getFamily();
// coordinator must be the first condition to evaluate because coordinators have the associated role as well
if ($user->hasRole(UserRolesEnum::ROLE_COORDINATOR_LONG)) {
switch ($family->getUserAccessType()) {
case UserGroupAccessType::ENABLE:
return $user->belongsToUsers($family->getUsers());
case UserGroupAccessType::DISABLE:
return !$user->belongsToUsers($family->getUsers());
case UserGroupAccessType::IGNORE:
default:
return true;
}
} elseif ($user->hasRole(UserRolesEnum::ROLE_ASSOCIATED_LONG)) {
switch ($family->getUserGroupAccessType()) {
case UserGroupAccessType::ENABLE:
return $user->belongsToUserGroup($family->getUserGroups());
case UserGroupAccessType::DISABLE:
return !$user->belongsToUserGroup($family->getUserGroups());
case UserGroupAccessType::IGNORE:
default:
return true;
}
} else {
return true;
}
}
public static function userHasAccessClosure(UserInterface $user): \Closure
{
return static function (Product $product) use ($user) {
$family = $product->getFamily();
// coordinator must be the first condition to evaluate because coordinators have the associated role as well
if ($user->hasRole(UserRolesEnum::ROLE_COORDINATOR_LONG)) {
switch ($family->getUserAccessType()) {
case UserGroupAccessType::ENABLE:
return $user->belongsToUsers($family->getUsers());
case UserGroupAccessType::DISABLE:
return !$user->belongsToUsers($family->getUsers());
case UserGroupAccessType::IGNORE:
default:
return true;
}
} elseif ($user->hasRole(UserRolesEnum::ROLE_ASSOCIATED_LONG)) {
switch ($family->getUserGroupAccessType()) {
case UserGroupAccessType::ENABLE:
return $user->belongsToUserGroup($family->getUserGroups());
case UserGroupAccessType::DISABLE:
return !$user->belongsToUserGroup($family->getUserGroups());
case UserGroupAccessType::IGNORE:
default:
return true;
}
} else {
return true;
}
};
}
public static function getProductsUserHasAccess(UserInterface $user, iterable $products): iterable
{
$productsUserHasAccess = [];
if (!empty($products)) {
// get only products that current user has access
/** @var Product $product */
foreach ($products as $product) {
if ($product->userHasAccess($user)) {
$productsUserHasAccess[] = $product;
}
}
}
return $productsUserHasAccess;
}
public function getRappel(): Money
{
$rappelPercentage = $this->getFamily()->getSupplier()->getRappelPercentage();
if ($this->hasSpecialPriceActive()) {
return $this->getSpecialPriceActive()->getPrice()->multiply($rappelPercentage / 100);
}
return $this->getRatePrice()->multiply($rappelPercentage / 100);
}
}