src/Entity/SharedEventCode.php line 21

  1. <?php
  2. declare(strict_types=1);
  3. namespace App\Entity;
  4. use App\Entity\User\EmailB2B;
  5. use App\Repository\SharedEventCodeRepository;
  6. use Doctrine\DBAL\Types\Types;
  7. use Doctrine\ORM\Mapping as ORM;
  8. use Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity;
  9. use Symfony\Component\Validator\Constraints as Assert;
  10. use Symfony\Component\Validator\Context\ExecutionContextInterface;
  11. /**
  12.  * Code événement partagé : même code pour tous les participants, période de validité, tarification paramétrable.
  13.  */
  14. #[ORM\Entity(repositoryClassSharedEventCodeRepository::class)]
  15. #[ORM\Table(name'app_shared_event_code')]
  16. #[UniqueEntity('code')]
  17. class SharedEventCode
  18. {
  19.     /** Fuseau métier pour les bornes début/fin (saisie BO + comparaison API). */
  20.     public const VALIDITY_TIMEZONE 'Europe/Paris';
  21.     public const PRICING_OFFERED 'offered';
  22.     public const PRICING_NORMAL 'normal';
  23.     public const PRICING_SPECIAL 'special';
  24.     public const FRAME_10X15 '10x15';
  25.     public const FRAME_15X10 '15x10';
  26.     #[ORM\Id]
  27.     #[ORM\GeneratedValue]
  28.     #[ORM\Column]
  29.     private ?int $id null;
  30.     #[ORM\Column(length50)]
  31.     #[Assert\NotBlank]
  32.     #[Assert\Length(min1max10)]
  33.     #[Assert\Regex(pattern'/^[A-Za-z0-9]+$/')]
  34.     private ?string $code null;
  35.     #[ORM\ManyToOne]
  36.     #[ORM\JoinColumn(nullabletrueonDelete'SET NULL')]
  37.     private ?EmailB2B $emailB2B null;
  38.     /** Événement B2B « classique » (admin Code événement) — pour rattacher le code partagé au bon client / campagne. */
  39.     #[ORM\ManyToOne]
  40.     #[ORM\JoinColumn(nullabletrueonDelete'SET NULL')]
  41.     private ?CodeEvent $codeEvent null;
  42.     #[ORM\Column(length255)]
  43.     #[Assert\NotBlank]
  44.     private ?string $name null;
  45.     #[ORM\Column(length255)]
  46.     #[Assert\NotBlank]
  47.     private ?string $nameInApp null;
  48.     #[ORM\Column(length255nullabletrue)]
  49.     private ?string $companyName null;
  50.     #[ORM\Column(typeTypes::DATETIME_MUTABLE)]
  51.     #[Assert\NotNull]
  52.     private ?\DateTimeInterface $startAt null;
  53.     #[ORM\Column(typeTypes::DATETIME_MUTABLE)]
  54.     #[Assert\NotNull]
  55.     private ?\DateTimeInterface $endAt null;
  56.     #[ORM\Column(length8)]
  57.     #[Assert\Choice(choices: [self::FRAME_10X15self::FRAME_15X10])]
  58.     private string $frameFormat self::FRAME_10X15;
  59.     /**
  60.      * Conservé en base pour compat ; la capacité réelle est **12 → 36** dans l’app (participant payeur = comme B2C).
  61.      * L’API use-code renvoie toujours 12 ; le BO ne propose plus de champ pour modifier cette valeur.
  62.      */
  63.     #[ORM\Column]
  64.     private int $size 12;
  65.     #[ORM\Column(length255)]
  66.     private string $country 'FR';
  67.     #[ORM\Column(length255)]
  68.     private string $filenameLogo '';
  69.     #[ORM\Column(length255nullabletrue)]
  70.     private ?string $filenameTemplate null;
  71.     #[ORM\Column(length255nullabletrue)]
  72.     private ?string $filenameTemplateVertical null;
  73.     #[ORM\Column(length255nullabletrue)]
  74.     private ?string $filenameAddonPhoto null;
  75.     #[ORM\Column(length20)]
  76.     #[Assert\Choice(choices: [self::PRICING_OFFEREDself::PRICING_NORMALself::PRICING_SPECIAL])]
  77.     private string $pricingType self::PRICING_OFFERED;
  78.     /**
  79.      * @deprecated Préférer specialDiscountPercent. Conservé pour d’anciens codes encore en prix fixe.
  80.      */
  81.     #[ORM\Column(nullabletrue)]
  82.     private ?int $specialPriceCents null;
  83.     /** Réduction en % sur le prix direct produit (1–100) si pricingType = special */
  84.     #[ORM\Column(nullabletrue)]
  85.     #[Assert\Range(min1max100)]
  86.     private ?int $specialDiscountPercent null;
  87.     /** Première fois qu’un participant a utilisé le code (rattachement B2B ou commande). */
  88.     #[ORM\Column(typeTypes::DATETIME_IMMUTABLEnullabletrue)]
  89.     private ?\DateTimeImmutable $firstUsedAt null;
  90.     public function getId(): ?int
  91.     {
  92.         return $this->id;
  93.     }
  94.     public function getCode(): ?string
  95.     {
  96.         return $this->code;
  97.     }
  98.     public function setCode(string $code): self
  99.     {
  100.         $this->code strtoupper(trim($code));
  101.         return $this;
  102.     }
  103.     public function getEmailB2B(): ?EmailB2B
  104.     {
  105.         return $this->emailB2B;
  106.     }
  107.     public function setEmailB2B(?EmailB2B $emailB2B): self
  108.     {
  109.         $this->emailB2B $emailB2B;
  110.         return $this;
  111.     }
  112.     public function getCodeEvent(): ?CodeEvent
  113.     {
  114.         return $this->codeEvent;
  115.     }
  116.     public function setCodeEvent(?CodeEvent $codeEvent): self
  117.     {
  118.         $this->codeEvent $codeEvent;
  119.         return $this;
  120.     }
  121.     public function getName(): ?string
  122.     {
  123.         return $this->name;
  124.     }
  125.     public function setName(string $name): self
  126.     {
  127.         $this->name $name;
  128.         return $this;
  129.     }
  130.     public function getNameInApp(): ?string
  131.     {
  132.         return $this->nameInApp;
  133.     }
  134.     public function setNameInApp(string $nameInApp): self
  135.     {
  136.         $this->nameInApp $nameInApp;
  137.         return $this;
  138.     }
  139.     public function getCompanyName(): ?string
  140.     {
  141.         return $this->companyName;
  142.     }
  143.     public function setCompanyName(?string $companyName): self
  144.     {
  145.         $this->companyName $companyName;
  146.         return $this;
  147.     }
  148.     public function getStartAt(): ?\DateTimeInterface
  149.     {
  150.         return $this->startAt;
  151.     }
  152.     public function setStartAt(\DateTimeInterface $startAt): self
  153.     {
  154.         $this->startAt $startAt;
  155.         return $this;
  156.     }
  157.     public function getEndAt(): ?\DateTimeInterface
  158.     {
  159.         return $this->endAt;
  160.     }
  161.     public function setEndAt(\DateTimeInterface $endAt): self
  162.     {
  163.         $this->endAt $endAt;
  164.         return $this;
  165.     }
  166.     public function getFrameFormat(): string
  167.     {
  168.         return $this->frameFormat;
  169.     }
  170.     public function setFrameFormat(string $frameFormat): self
  171.     {
  172.         $this->frameFormat $frameFormat;
  173.         return $this;
  174.     }
  175.     public function getSize(): int
  176.     {
  177.         return $this->size;
  178.     }
  179.     public function setSize(int $size): self
  180.     {
  181.         $this->size $size;
  182.         return $this;
  183.     }
  184.     public function getCountry(): string
  185.     {
  186.         return $this->country;
  187.     }
  188.     public function setCountry(string $country): self
  189.     {
  190.         $this->country $country;
  191.         return $this;
  192.     }
  193.     public function getFilenameLogo(): string
  194.     {
  195.         return $this->filenameLogo;
  196.     }
  197.     public function setFilenameLogo(string $filenameLogo): self
  198.     {
  199.         $this->filenameLogo $filenameLogo;
  200.         return $this;
  201.     }
  202.     public function getFilenameTemplate(): ?string
  203.     {
  204.         return $this->filenameTemplate;
  205.     }
  206.     public function setFilenameTemplate(?string $filenameTemplate): self
  207.     {
  208.         $this->filenameTemplate $filenameTemplate;
  209.         return $this;
  210.     }
  211.     public function getFilenameTemplateVertical(): ?string
  212.     {
  213.         return $this->filenameTemplateVertical;
  214.     }
  215.     public function setFilenameTemplateVertical(?string $filenameTemplateVertical): self
  216.     {
  217.         $this->filenameTemplateVertical $filenameTemplateVertical;
  218.         return $this;
  219.     }
  220.     public function getFilenameAddonPhoto(): ?string
  221.     {
  222.         return $this->filenameAddonPhoto;
  223.     }
  224.     public function setFilenameAddonPhoto(?string $filenameAddonPhoto): self
  225.     {
  226.         $this->filenameAddonPhoto $filenameAddonPhoto;
  227.         return $this;
  228.     }
  229.     public function getPricingType(): string
  230.     {
  231.         return $this->pricingType;
  232.     }
  233.     public function setPricingType(string $pricingType): self
  234.     {
  235.         $this->pricingType $pricingType;
  236.         return $this;
  237.     }
  238.     public function getSpecialPriceCents(): ?int
  239.     {
  240.         return $this->specialPriceCents;
  241.     }
  242.     public function setSpecialPriceCents(?int $specialPriceCents): self
  243.     {
  244.         $this->specialPriceCents $specialPriceCents;
  245.         return $this;
  246.     }
  247.     public function getSpecialDiscountPercent(): ?int
  248.     {
  249.         return $this->specialDiscountPercent;
  250.     }
  251.     public function setSpecialDiscountPercent(?int $specialDiscountPercent): self
  252.     {
  253.         $this->specialDiscountPercent $specialDiscountPercent;
  254.         return $this;
  255.     }
  256.     /**
  257.      * Les dates en base (DATETIME) et le formulaire admin sont interprétées comme heure locale France.
  258.      * Compare à l’instant courant dans ce même fuseau, pour éviter les décalages si PHP/serveur est en UTC.
  259.      */
  260.     public function isValidAt(?\DateTimeInterface $now null): bool
  261.     {
  262.         $start $this->startAt;
  263.         $end $this->endAt;
  264.         if ($start === null || $end === null) {
  265.             return false;
  266.         }
  267.         $tz = new \DateTimeZone(self::VALIDITY_TIMEZONE);
  268.         $startParis = new \DateTimeImmutable($start->format('Y-m-d H:i:s'), $tz);
  269.         $endParis = new \DateTimeImmutable($end->format('Y-m-d H:i:s'), $tz);
  270.         $nowParis $now === null
  271.             ? new \DateTimeImmutable('now'$tz)
  272.             : \DateTimeImmutable::createFromInterface($now)->setTimezone($tz);
  273.         return $startParis <= $nowParis && $nowParis <= $endParis;
  274.     }
  275.     public function getFirstUsedAt(): ?\DateTimeImmutable
  276.     {
  277.         return $this->firstUsedAt;
  278.     }
  279.     public function setFirstUsedAt(?\DateTimeImmutable $firstUsedAt): self
  280.     {
  281.         $this->firstUsedAt $firstUsedAt;
  282.         return $this;
  283.     }
  284.     /** Enregistre la première utilisation (idempotent). */
  285.     public function markFirstUseIfUnset(): void
  286.     {
  287.         if ($this->firstUsedAt !== null) {
  288.             return;
  289.         }
  290.         $this->firstUsedAt = new \DateTimeImmutable();
  291.     }
  292.     public function isUsed(): bool
  293.     {
  294.         return $this->firstUsedAt !== null;
  295.     }
  296.     #[Assert\Callback]
  297.     public function validateSpecialPricing(ExecutionContextInterface $context): void
  298.     {
  299.         if (self::PRICING_SPECIAL !== $this->pricingType) {
  300.             return;
  301.         }
  302.         $okPercent $this->specialDiscountPercent !== null
  303.             && $this->specialDiscountPercent >= 1
  304.             && $this->specialDiscountPercent <= 100;
  305.         $okLegacy $this->specialPriceCents !== null && $this->specialPriceCents 0;
  306.         if (!$okPercent && !$okLegacy) {
  307.             $context->buildViolation('Pour le tarif spécial, renseignez une réduction entre 1 et 100 %.')
  308.                 ->atPath('specialDiscountPercent')
  309.                 ->addViolation();
  310.         }
  311.     }
  312. }