From b420b4e633e8f67dfa2af81a3a0c17c59b327dd6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ma=C3=ABl=20Gangloff?= Date: Fri, 31 Oct 2025 13:40:09 +0100 Subject: [PATCH] feat: add domain_purchase table --- migrations/Version20251031115210.php | 47 +++++++ src/Entity/Connector.php | 37 +++++ src/Entity/Domain.php | 43 ++++++ src/Entity/DomainPurchase.php | 132 ++++++++++++++++++ src/Entity/User.php | 37 +++++ src/Message/OrderDomain.php | 1 + src/MessageHandler/OrderDomainHandler.php | 24 +++- .../ProcessWatchlistHandler.php | 9 +- src/MessageHandler/UpdateDomainHandler.php | 2 +- src/Repository/DomainPurchaseRepository.php | 43 ++++++ .../Service/Provider/AbstractProviderTest.php | 2 +- 11 files changed, 371 insertions(+), 6 deletions(-) create mode 100644 migrations/Version20251031115210.php create mode 100644 src/Entity/DomainPurchase.php create mode 100644 src/Repository/DomainPurchaseRepository.php diff --git a/migrations/Version20251031115210.php b/migrations/Version20251031115210.php new file mode 100644 index 0000000..cfc0d8e --- /dev/null +++ b/migrations/Version20251031115210.php @@ -0,0 +1,47 @@ +addSql('CREATE TABLE domain_purchase (id UUID NOT NULL, domain_id VARCHAR(255) NOT NULL, connector_id UUID DEFAULT NULL, user_id INT DEFAULT NULL, domain_updated_at TIMESTAMP(0) WITHOUT TIME ZONE NOT NULL, domain_ordered_at TIMESTAMP(0) WITHOUT TIME ZONE DEFAULT NULL, connector_provider VARCHAR(255) NOT NULL, domain_deleted_at TIMESTAMP(0) WITHOUT TIME ZONE NOT NULL, PRIMARY KEY(id))'); + $this->addSql('CREATE INDEX IDX_72999E74115F0EE5 ON domain_purchase (domain_id)'); + $this->addSql('CREATE INDEX IDX_72999E744D085745 ON domain_purchase (connector_id)'); + $this->addSql('CREATE INDEX IDX_72999E74A76ED395 ON domain_purchase (user_id)'); + $this->addSql('COMMENT ON COLUMN domain_purchase.id IS \'(DC2Type:uuid)\''); + $this->addSql('COMMENT ON COLUMN domain_purchase.connector_id IS \'(DC2Type:uuid)\''); + $this->addSql('COMMENT ON COLUMN domain_purchase.domain_updated_at IS \'(DC2Type:datetime_immutable)\''); + $this->addSql('COMMENT ON COLUMN domain_purchase.domain_ordered_at IS \'(DC2Type:datetime_immutable)\''); + $this->addSql('COMMENT ON COLUMN domain_purchase.domain_deleted_at IS \'(DC2Type:datetime_immutable)\''); + $this->addSql('ALTER TABLE domain_purchase ADD CONSTRAINT FK_72999E74115F0EE5 FOREIGN KEY (domain_id) REFERENCES domain (ldh_name) NOT DEFERRABLE INITIALLY IMMEDIATE'); + $this->addSql('ALTER TABLE domain_purchase ADD CONSTRAINT FK_72999E744D085745 FOREIGN KEY (connector_id) REFERENCES connector (id) NOT DEFERRABLE INITIALLY IMMEDIATE'); + $this->addSql('ALTER TABLE domain_purchase ADD CONSTRAINT FK_72999E74A76ED395 FOREIGN KEY (user_id) REFERENCES "user" (id) NOT DEFERRABLE INITIALLY IMMEDIATE'); + $this->addSql('ALTER TABLE watchlist ALTER enabled DROP DEFAULT'); + } + + public function down(Schema $schema): void + { + // this down() migration is auto-generated, please modify it to your needs + $this->addSql('ALTER TABLE domain_purchase DROP CONSTRAINT FK_72999E74115F0EE5'); + $this->addSql('ALTER TABLE domain_purchase DROP CONSTRAINT FK_72999E744D085745'); + $this->addSql('ALTER TABLE domain_purchase DROP CONSTRAINT FK_72999E74A76ED395'); + $this->addSql('DROP TABLE domain_purchase'); + $this->addSql('ALTER TABLE watchlist ALTER enabled SET DEFAULT true'); + } +} diff --git a/src/Entity/Connector.php b/src/Entity/Connector.php index e4b1740..9aae2a1 100644 --- a/src/Entity/Connector.php +++ b/src/Entity/Connector.php @@ -73,10 +73,17 @@ class Connector #[Groups(['connector:list'])] protected int $watchlistCount; + /** + * @var Collection + */ + #[ORM\OneToMany(targetEntity: DomainPurchase::class, mappedBy: 'connector')] + private Collection $domainPurchases; + public function __construct() { $this->id = Uuid::v4(); $this->watchlists = new ArrayCollection(); + $this->domainPurchases = new ArrayCollection(); } public function getId(): ?string @@ -166,4 +173,34 @@ class Connector { return $this->watchlists->count(); } + + /** + * @return Collection + */ + public function getDomainPurchases(): Collection + { + return $this->domainPurchases; + } + + public function addDomainPurchase(DomainPurchase $domainPurchase): static + { + if (!$this->domainPurchases->contains($domainPurchase)) { + $this->domainPurchases->add($domainPurchase); + $domainPurchase->setConnector($this); + } + + return $this; + } + + public function removeDomainPurchase(DomainPurchase $domainPurchase): static + { + if ($this->domainPurchases->removeElement($domainPurchase)) { + // set the owning side to null (unless already changed) + if ($domainPurchase->getConnector() === $this) { + $domainPurchase->setConnector(null); + } + } + + return $this; + } } diff --git a/src/Entity/Domain.php b/src/Entity/Domain.php index 819fe6c..8e616a8 100644 --- a/src/Entity/Domain.php +++ b/src/Entity/Domain.php @@ -156,6 +156,12 @@ class Domain private ?int $expiresInDays; + /** + * @var Collection + */ + #[ORM\OneToMany(targetEntity: DomainPurchase::class, mappedBy: 'domain', orphanRemoval: true)] + private Collection $domainPurchases; + private const IMPORTANT_EVENTS = [EventAction::Deletion->value, EventAction::Expiration->value]; private const IMPORTANT_STATUS = [ 'redemption period', @@ -179,6 +185,7 @@ class Domain $this->deleted = false; $this->domainStatuses = new ArrayCollection(); $this->dnsKey = new ArrayCollection(); + $this->domainPurchases = new ArrayCollection(); } public function getLdhName(): ?string @@ -516,4 +523,40 @@ class Domain return $this; } + + /** + * @return Collection + */ + public function getDomainPurchases(): Collection + { + return $this->domainPurchases; + } + + public function addDomainPurchase(DomainPurchase $domainPurchase): static + { + if (!$this->domainPurchases->contains($domainPurchase)) { + $this->domainPurchases->add($domainPurchase); + $domainPurchase->setDomain($this); + } + + return $this; + } + + public function removeDomainPurchase(DomainPurchase $domainPurchase): static + { + if ($this->domainPurchases->removeElement($domainPurchase)) { + // set the owning side to null (unless already changed) + if ($domainPurchase->getDomain() === $this) { + $domainPurchase->setDomain(null); + } + } + + return $this; + } + + #[Groups(['domain:item', 'domain:list'])] + public function getPurchaseCount(): ?int + { + return $this->domainPurchases->count(); + } } diff --git a/src/Entity/DomainPurchase.php b/src/Entity/DomainPurchase.php new file mode 100644 index 0000000..3d6c1c6 --- /dev/null +++ b/src/Entity/DomainPurchase.php @@ -0,0 +1,132 @@ +id = Uuid::v4(); + } + + public function getId(): ?string + { + return $this->id; + } + + public function getDomain(): ?Domain + { + return $this->domain; + } + + public function setDomain(?Domain $domain): static + { + $this->domain = $domain; + + return $this; + } + + public function getDomainUpdatedAt(): ?\DateTimeImmutable + { + return $this->domainUpdatedAt; + } + + public function setDomainUpdatedAt(\DateTimeImmutable $domainUpdatedAt): static + { + $this->domainUpdatedAt = $domainUpdatedAt; + + return $this; + } + + public function getDomainOrderedAt(): ?\DateTimeImmutable + { + return $this->domainOrderedAt; + } + + public function setDomainOrderedAt(?\DateTimeImmutable $domainOrderedAt): static + { + $this->domainOrderedAt = $domainOrderedAt; + + return $this; + } + + public function getConnectorProvider(): ?ConnectorProvider + { + return $this->connectorProvider; + } + + public function setConnectorProvider(ConnectorProvider $connectorProvider): static + { + $this->connectorProvider = $connectorProvider; + + return $this; + } + + public function getConnector(): ?Connector + { + return $this->connector; + } + + public function setConnector(?Connector $connector): static + { + $this->connector = $connector; + + return $this; + } + + public function getUser(): ?User + { + return $this->user; + } + + public function setUser(?User $user): static + { + $this->user = $user; + + return $this; + } + + public function getDomainDeletedAt(): ?\DateTimeImmutable + { + return $this->domainDeletedAt; + } + + public function setDomainDeletedAt(\DateTimeImmutable $domainDeletedAt): static + { + $this->domainDeletedAt = $domainDeletedAt; + + return $this; + } +} diff --git a/src/Entity/User.php b/src/Entity/User.php index 29fa9f5..e788ef9 100644 --- a/src/Entity/User.php +++ b/src/Entity/User.php @@ -89,10 +89,17 @@ class User implements UserInterface, PasswordAuthenticatedUserInterface #[Groups(['user:register'])] private ?string $plainPassword = null; + /** + * @var Collection + */ + #[ORM\OneToMany(targetEntity: DomainPurchase::class, mappedBy: 'user')] + private Collection $domainPurchases; + public function __construct() { $this->watchlists = new ArrayCollection(); $this->connectors = new ArrayCollection(); + $this->domainPurchases = new ArrayCollection(); } public function getId(): ?int @@ -262,4 +269,34 @@ class User implements UserInterface, PasswordAuthenticatedUserInterface return $this; } + + /** + * @return Collection + */ + public function getDomainPurchases(): Collection + { + return $this->domainPurchases; + } + + public function addDomainPurchase(DomainPurchase $domainPurchase): static + { + if (!$this->domainPurchases->contains($domainPurchase)) { + $this->domainPurchases->add($domainPurchase); + $domainPurchase->setUser($this); + } + + return $this; + } + + public function removeDomainPurchase(DomainPurchase $domainPurchase): static + { + if ($this->domainPurchases->removeElement($domainPurchase)) { + // set the owning side to null (unless already changed) + if ($domainPurchase->getUser() === $this) { + $domainPurchase->setUser(null); + } + } + + return $this; + } } diff --git a/src/Message/OrderDomain.php b/src/Message/OrderDomain.php index 61ec9ad..6d51d05 100644 --- a/src/Message/OrderDomain.php +++ b/src/Message/OrderDomain.php @@ -7,6 +7,7 @@ final class OrderDomain public function __construct( public string $watchlistToken, public string $ldhName, + public \DateTimeImmutable $updatedAt, ) { } } diff --git a/src/MessageHandler/OrderDomainHandler.php b/src/MessageHandler/OrderDomainHandler.php index 72c6fd3..6f0c7c1 100644 --- a/src/MessageHandler/OrderDomainHandler.php +++ b/src/MessageHandler/OrderDomainHandler.php @@ -3,6 +3,7 @@ namespace App\MessageHandler; use App\Entity\Domain; +use App\Entity\DomainPurchase; use App\Entity\Watchlist; use App\Message\OrderDomain; use App\Notifier\DomainOrderErrorNotification; @@ -13,6 +14,7 @@ use App\Service\ChatNotificationService; use App\Service\InfluxdbService; use App\Service\Provider\AbstractProvider; use App\Service\StatService; +use Doctrine\ORM\EntityManagerInterface; use Psr\Log\LoggerInterface; use Symfony\Component\DependencyInjection\Attribute\Autowire; use Symfony\Component\DependencyInjection\ContainerInterface; @@ -42,7 +44,7 @@ final readonly class OrderDomainHandler #[Autowire(service: 'service_container')] private ContainerInterface $locator, #[Autowire(param: 'influxdb_enabled')] - private bool $influxdbEnabled, + private bool $influxdbEnabled, private EntityManagerInterface $em, ) { $this->sender = new Address($mailerSenderEmail, $mailerSenderName); } @@ -114,6 +116,16 @@ final readonly class OrderDomainHandler $notification = (new DomainOrderNotification($this->sender, $domain, $connector)); $this->mailer->send($notification->asEmailMessage(new Recipient($watchlist->getUser()->getEmail()))->getMessage()); $this->chatNotificationService->sendChatNotification($watchlist, $notification); + + $this->em->persist((new DomainPurchase()) + ->setDomain($domain) + ->setConnector($connector) + ->setConnectorProvider($connector->getProvider()) + ->setDomainOrderedAt(new \DateTimeImmutable()) + ->setUser($watchlist->getUser()) + ->setDomainDeletedAt($domain->getUpdatedAt()) + ->setDomainUpdatedAt($message->updatedAt)); + $this->em->flush(); } catch (\Throwable $exception) { /* * The purchase was not successful (for several possible reasons that we have not determined). @@ -134,6 +146,16 @@ final readonly class OrderDomainHandler $this->mailer->send($notification->asEmailMessage(new Recipient($watchlist->getUser()->getEmail()))->getMessage()); $this->chatNotificationService->sendChatNotification($watchlist, $notification); + $this->em->persist((new DomainPurchase()) + ->setDomain($domain) + ->setConnector($connector) + ->setConnectorProvider($connector->getProvider()) + ->setDomainOrderedAt(null) + ->setUser($watchlist->getUser()) + ->setDomainDeletedAt($domain->getUpdatedAt()) + ->setDomainUpdatedAt($message->updatedAt)); + $this->em->flush(); + throw $exception; } } diff --git a/src/MessageHandler/ProcessWatchlistHandler.php b/src/MessageHandler/ProcessWatchlistHandler.php index 0af5c26..c2ef749 100644 --- a/src/MessageHandler/ProcessWatchlistHandler.php +++ b/src/MessageHandler/ProcessWatchlistHandler.php @@ -7,6 +7,7 @@ use App\Entity\Watchlist; use App\Message\OrderDomain; use App\Message\ProcessWatchlist; use App\Message\UpdateDomain; +use App\Repository\DomainRepository; use App\Repository\WatchlistRepository; use App\Service\Provider\AbstractProvider; use App\Service\Provider\CheckDomainProviderInterface; @@ -26,7 +27,7 @@ final readonly class ProcessWatchlistHandler private WatchlistRepository $watchlistRepository, private LoggerInterface $logger, #[Autowire(service: 'service_container')] - private ContainerInterface $locator, + private ContainerInterface $locator, private DomainRepository $domainRepository, ) { } @@ -64,8 +65,10 @@ final readonly class ProcessWatchlistHandler throw $exception; } - foreach ($checkedDomains as $domain) { - $this->bus->dispatch(new OrderDomain($watchlist->getToken(), $domain)); + /** @var string $ldhName */ + foreach ($checkedDomains as $ldhName) { + $domain = $this->domainRepository->findOneBy(['ldhName' => $ldhName]); + $this->bus->dispatch(new OrderDomain($watchlist->getToken(), $domain->getLdhName(), $domain->getUpdatedAt())); } return; diff --git a/src/MessageHandler/UpdateDomainHandler.php b/src/MessageHandler/UpdateDomainHandler.php index bb12cf1..232c3c2 100644 --- a/src/MessageHandler/UpdateDomainHandler.php +++ b/src/MessageHandler/UpdateDomainHandler.php @@ -131,7 +131,7 @@ final readonly class UpdateDomainHandler * If the domain name no longer appears in the WHOIS AND a connector is associated with this Watchlist, * this connector is used to purchase the domain name. */ - $this->bus->dispatch(new OrderDomain($watchlist->getToken(), $domain->getLdhName())); + $this->bus->dispatch(new OrderDomain($watchlist->getToken(), $domain->getLdhName(), $updatedAt)); } } catch (TldNotSupportedException|UnknownRdapServerException) { /* diff --git a/src/Repository/DomainPurchaseRepository.php b/src/Repository/DomainPurchaseRepository.php new file mode 100644 index 0000000..3772893 --- /dev/null +++ b/src/Repository/DomainPurchaseRepository.php @@ -0,0 +1,43 @@ + + */ +class DomainPurchaseRepository extends ServiceEntityRepository +{ + public function __construct(ManagerRegistry $registry) + { + parent::__construct($registry, DomainPurchase::class); + } + + // /** + // * @return DomainPurchase[] Returns an array of DomainPurchase objects + // */ + // public function findByExampleField($value): array + // { + // return $this->createQueryBuilder('d') + // ->andWhere('d.exampleField = :val') + // ->setParameter('val', $value) + // ->orderBy('d.id', 'ASC') + // ->setMaxResults(10) + // ->getQuery() + // ->getResult() + // ; + // } + + // public function findOneBySomeField($value): ?DomainPurchase + // { + // return $this->createQueryBuilder('d') + // ->andWhere('d.exampleField = :val') + // ->setParameter('val', $value) + // ->getQuery() + // ->getOneOrNullResult() + // ; + // } +} diff --git a/tests/Service/Provider/AbstractProviderTest.php b/tests/Service/Provider/AbstractProviderTest.php index 713d7ac..05c2aef 100644 --- a/tests/Service/Provider/AbstractProviderTest.php +++ b/tests/Service/Provider/AbstractProviderTest.php @@ -109,7 +109,7 @@ class AbstractProviderTest extends ApiTestCase // Trigger the Order Domain message $orderDomainHandler = self::getContainer()->get(OrderDomainHandler::class); - $message = new OrderDomain($watchlist->getToken(), $domain->getLdhName()); + $message = new OrderDomain($watchlist->getToken(), $domain->getLdhName(), $domain->getUpdatedAt()); $orderDomainHandler($message); $this->assertResponseStatusCodeSame(200);