<?php
namespace App\Entity;
use App\Constants\ActiveConstants;
use App\Constants\VoucherConstants;
use App\Repository\VoucherRepository;
use App\Traits\TimeTrackTrait;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity;
use Symfony\Component\Validator\Constraints as Assert;
use Symfony\Component\Validator\Context\ExecutionContextInterface;
use Symfony\Contracts\Translation\TranslatorInterface;
use App\Validator\Constraints as AppAssert;
#[ORM\Entity(repositoryClass: VoucherRepository::class)]
#[ORM\Table(name: 'voucher')]
#[ORM\HasLifecycleCallbacks]
#[ORM\EntityListeners(["App\EventListener\VoucherCodeGeneratorListener"])]
#[UniqueEntity(
fields: ['code'],
message: 'Такий код вже використовується.'
)]
class Voucher
{
use TimeTrackTrait;
#[ORM\Id]
#[ORM\GeneratedValue]
#[ORM\Column]
private ?int $id = null;
#[ORM\Column(length: 255)]
#[Assert\NotBlank(message: 'Поле "Назва" є обов’язковим.')]
private string $name;
#[ORM\Column(length: 50, nullable: true, unique: true, options: ['collation' => 'utf8mb4_bin'])]
#[AppAssert\UniqueVoucherCode]
private ?string $code = null;
#[ORM\Column(length: 50)]
#[Assert\NotBlank(message: 'Оберіть тип знижки.')]
private string $type;
#[ORM\Column(length: 50, name: "voucher_type")]
private string $voucherType = VoucherConstants::VOUCHER_SINGLE;
#[ORM\Column(type: "decimal", precision: 10, scale: 2)]
#[Assert\NotBlank(message: 'Вкажіть суму знижки.')]
#[Assert\Positive(message: 'Сума знижки повинна бути більшою за 0.')]
private float $value;
#[ORM\Column(type: "datetime", nullable: true, name: "start_at")]
private ?\DateTimeInterface $startAt = null;
#[ORM\Column(type: "datetime", nullable: true, name: "expires_at")]
#[Assert\Expression(
"this.getExpiresAt() === null or this.getStartAt() === null or this.getExpiresAt() > this.getStartAt()",
message: "Дата закінчення повинна бути пізнішою за дату початку."
)]
private ?\DateTimeInterface $expiresAt = null;
#[ORM\Column(type: "boolean")]
private bool $active = true;
#[ORM\Column(type: "integer", nullable: true, name: "usage_per_user")]
private ?int $usagePerUser = null;
#[ORM\Column(type: "text", nullable: true)]
private ?string $description = null;
#[ORM\OneToMany(mappedBy: "voucher", targetEntity: VoucherCode::class, cascade: ["persist", "remove"], orphanRemoval: true)]
private Collection $codes;
#[ORM\OneToMany(mappedBy: 'voucher', targetEntity: Order::class)]
private Collection $orders;
#[ORM\ManyToMany(targetEntity: Company::class)]
#[ORM\JoinTable(
name: 'voucher_company',
joinColumns: [new ORM\JoinColumn(name: 'voucher_id', referencedColumnName: 'id', onDelete: 'CASCADE')],
inverseJoinColumns: [new ORM\JoinColumn(name: 'company_id', referencedColumnName: 'id', onDelete: 'CASCADE')]
)]
private Collection $companies;
#[ORM\ManyToMany(targetEntity: Tariff::class)]
#[ORM\JoinTable(
name: 'voucher_tariff',
joinColumns: [new ORM\JoinColumn(name: 'voucher_id', referencedColumnName: 'id', onDelete: 'CASCADE')],
inverseJoinColumns: [new ORM\JoinColumn(name: 'tariff_id', referencedColumnName: 'id', onDelete: 'CASCADE')]
)]
private Collection $tariffs;
private ?int $generatedCount = null;
private ?int $generatedLength = null;
public function __construct()
{
$this->companies = new ArrayCollection();
$this->tariffs = new ArrayCollection();
$this->orders = new ArrayCollection();
$this->codes = new ArrayCollection();
}
/**
* @return Collection<int, Order>
*/
public function getOrders(): Collection
{
return $this->orders;
}
public function addOrder(Order $order): static
{
if (!$this->orders->contains($order)) {
$this->orders->add($order);
$order->setVoucher($this);
}
return $this;
}
public function removeOrder(Order $order): static
{
if ($this->orders->removeElement($order)) {
if ($order->getVoucher() === $this) {
$order->setVoucher(null);
}
}
return $this;
}
public function getId(): ?int
{
return $this->id;
}
public function getName(): string
{
return $this->name;
}
public function setName(string $name): self
{
$this->name = $name;
return $this;
}
public function getCode(): ?string
{
return $this->code;
}
public function setCode(?string $code): self
{
if ($this->voucherType !== VoucherConstants::VOUCHER_GENERATED) {
$this->code = $code;
}
return $this;
}
public function getType(): string
{
return $this->type;
}
public function setType(string $type): self
{
$this->type = $type;
return $this;
}
public function getVoucherType(): string
{
return $this->voucherType;
}
public function setVoucherType(string $voucherType): self
{
$this->voucherType = $voucherType;
return $this;
}
public function getValue(): float
{
return $this->value;
}
public function setValue(float $value): self
{
$this->value = $value;
return $this;
}
public function getStartAt(): ?\DateTimeInterface
{
return $this->startAt;
}
public function setStartAt(?\DateTimeInterface $startAt): self
{
$this->startAt = $startAt;
return $this;
}
public function getExpiresAt(): ?\DateTimeInterface
{
return $this->expiresAt;
}
public function setExpiresAt(?\DateTimeInterface $expiresAt): self
{
$this->expiresAt = $expiresAt;
return $this;
}
public function isActive(): bool
{
return $this->active;
}
public function setActive(bool $active): self
{
$this->active = $active;
return $this;
}
public function getActiveLabel(): string
{
return $this->active ? ActiveConstants::LABEL_YES : ActiveConstants::LABEL_NO;
}
public function getTypeLabel(): string
{
$types = VoucherConstants::loadTypes();
return $types[$this->type] ?? $this->type;
}
public function getVoucherTypeLabel(): string
{
$types = VoucherConstants::loadVoucherTypes();
return $types[$this->voucherType] ?? $this->voucherType;
}
public function getUsagePerUser(): ?int
{
return $this->usagePerUser;
}
public function setUsagePerUser(?int $usagePerUser): self
{
$this->usagePerUser = $usagePerUser;
return $this;
}
public function getDescription(): ?string
{
return $this->description;
}
public function setDescription(?string $description): self
{
$this->description = $description;
return $this;
}
/**
* @return Collection<int, VoucherCode>
*/
public function getCodes(): Collection
{
return $this->codes;
}
public function addCode(VoucherCode $code): self
{
if (!$this->codes->contains($code)) {
$this->codes[] = $code;
$code->setVoucher($this);
}
return $this;
}
public function removeCode(VoucherCode $code): self
{
if ($this->codes->removeElement($code)) {
if ($code->getVoucher() === $this) {
$code->setVoucher(null);
}
}
return $this;
}
/**
* Set createdAt.
*
* @param \DateTime|null $createdAt
*
* @return Voucher
*/
public function setCreatedAt($createdAt = null)
{
$this->createdAt = $createdAt;
return $this;
}
/**
* Get createdAt.
*
* @return \DateTime|null
*/
public function getCreatedAt()
{
return $this->createdAt;
}
/**
* Set updatedAt.
*
* @param \DateTime|null $updatedAt
*
* @return Voucher
*/
public function setUpdatedAt($updatedAt = null)
{
$this->updatedAt = $updatedAt;
return $this;
}
/**
* Get updatedAt.
*
* @return \DateTime|null
*/
public function getUpdatedAt()
{
return $this->updatedAt;
}
public function getCompanies(): Collection
{
return $this->companies;
}
public function addCompany(Company $company): self
{
if (!$this->companies->contains($company)) {
$this->companies->add($company);
}
return $this;
}
public function removeCompany(Company $company): self
{
$this->companies->removeElement($company);
return $this;
}
public function getTariffs(): Collection
{
return $this->tariffs;
}
public function addTariff(Tariff $tariff): self
{
if (!$this->tariffs->contains($tariff)) {
$this->tariffs->add($tariff);
}
return $this;
}
public function removeTariff(Tariff $tariff): self
{
$this->tariffs->removeElement($tariff);
return $this;
}
public function getGeneratedCount(): ?int
{
return $this->generatedCount;
}
public function setGeneratedCount(?int $generatedCount): self
{
$this->generatedCount = $generatedCount;
return $this;
}
public function getGeneratedLength(): ?int
{
return $this->generatedLength;
}
public function setGeneratedLength(?int $generatedLength): self
{
$this->generatedLength = $generatedLength;
return $this;
}
#[Assert\Callback]
public function validateSingleVoucherFields(ExecutionContextInterface $context): void
{
if ($this->voucherType === VoucherConstants::VOUCHER_SINGLE) {
if (empty($this->code)) {
$context->buildViolation('Поле "Код" є обов’язковим для Єдиного промокоду.')
->atPath('code')
->addViolation();
}
if (empty($this->usagePerUser) || $this->usagePerUser <= 0) {
$context->buildViolation('Поле "Кількість використань на користувача" має бути більше 0 для Єдиного промокоду.')
->atPath('usagePerUser')
->addViolation();
}
}
if ($this->voucherType === VoucherConstants::VOUCHER_GENERATED) {
if (empty($this->generatedCount) || $this->generatedCount <= 0) {
$context->buildViolation('Вкажіть кількість кодів для генерації.')
->atPath('generatedCount')
->addViolation();
}
if (empty($this->generatedLength) || $this->generatedLength <= 0) {
$context->buildViolation('Вкажіть довжину промокоду для генерації.')
->atPath('generatedLength')
->addViolation();
}
}
}
public function applyTo(float $amount, TranslatorInterface $translator): array
{
$original = round($amount, 2, PHP_ROUND_HALF_DOWN);
$discount = 0;
if (!$this->isUsable()) {
return [
'status' => 'error',
'errors' => ['voucher' => $translator->trans('voucher_not_found', [], 'validators')],
];
}
if ($this->getType() === VoucherConstants::TYPE_PERCENT) {
$discount = round($original * ($this->getValue() / 100.0), 2, PHP_ROUND_HALF_DOWN);
} elseif ($this->getType() === VoucherConstants::TYPE_FIXED) {
$discount = round($this->getValue(), 2, PHP_ROUND_HALF_DOWN);
}
$final = round($original - $discount, 2,PHP_ROUND_HALF_DOWN);
if ($discount >= $original || $final < 1) {
return [
'status' => 'error',
'errors' => ['voucher' => $translator->trans('voucher_not_found', [], 'validators')],
];
}
return [
'status' => 'ok',
'discount' => $discount,
'name' => $this->getName(),
'discount_type' => $this->getType(),
'price' => $final,
];
}
public function isExpired(): bool
{
return $this->expiresAt !== null && $this->expiresAt < new \DateTimeImmutable('now');
}
public function isNotStarted(): bool
{
return $this->startAt !== null && $this->startAt > new \DateTimeImmutable('now');
}
public function isUsable(): bool
{
return $this->isActive() && !$this->isExpired() && !$this->isNotStarted();
}
}