feat: add domain_purchase table

This commit is contained in:
Maël Gangloff 2025-10-31 13:40:09 +01:00
parent 2aeb897e6c
commit b420b4e633
No known key found for this signature in database
GPG Key ID: 11FDC81C24A7F629
11 changed files with 371 additions and 6 deletions

View File

@ -0,0 +1,47 @@
<?php
declare(strict_types=1);
namespace DoctrineMigrations;
use Doctrine\DBAL\Schema\Schema;
use Doctrine\Migrations\AbstractMigration;
/**
* Auto-generated Migration: Please modify to your needs!
*/
final class Version20251031115210 extends AbstractMigration
{
public function getDescription(): string
{
return 'Add domain_purchase';
}
public function up(Schema $schema): void
{
// this up() migration is auto-generated, please modify it to your needs
$this->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');
}
}

View File

@ -73,10 +73,17 @@ class Connector
#[Groups(['connector:list'])]
protected int $watchlistCount;
/**
* @var Collection<int, DomainPurchase>
*/
#[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<int, DomainPurchase>
*/
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;
}
}

View File

@ -156,6 +156,12 @@ class Domain
private ?int $expiresInDays;
/**
* @var Collection<int, DomainPurchase>
*/
#[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<int, DomainPurchase>
*/
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();
}
}

View File

@ -0,0 +1,132 @@
<?php
namespace App\Entity;
use App\Config\ConnectorProvider;
use App\Repository\DomainPurchaseRepository;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Uid\Uuid;
#[ORM\Entity(repositoryClass: DomainPurchaseRepository::class)]
class DomainPurchase
{
#[ORM\Id]
#[ORM\Column(type: 'uuid')]
private ?string $id;
#[ORM\ManyToOne(inversedBy: 'domainPurchases')]
#[ORM\JoinColumn(referencedColumnName: 'ldh_name', nullable: false)]
private ?Domain $domain = null;
#[ORM\Column]
private ?\DateTimeImmutable $domainUpdatedAt = null;
#[ORM\Column(nullable: true)]
private ?\DateTimeImmutable $domainOrderedAt = null;
#[ORM\Column(enumType: ConnectorProvider::class)]
private ?ConnectorProvider $connectorProvider = null;
#[ORM\ManyToOne(inversedBy: 'domainPurchases')]
private ?Connector $connector = null;
#[ORM\ManyToOne(inversedBy: 'domainPurchases')]
private ?User $user = null;
#[ORM\Column]
private ?\DateTimeImmutable $domainDeletedAt = null;
public function __construct()
{
$this->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;
}
}

View File

@ -89,10 +89,17 @@ class User implements UserInterface, PasswordAuthenticatedUserInterface
#[Groups(['user:register'])]
private ?string $plainPassword = null;
/**
* @var Collection<int, DomainPurchase>
*/
#[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<int, DomainPurchase>
*/
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;
}
}

View File

@ -7,6 +7,7 @@ final class OrderDomain
public function __construct(
public string $watchlistToken,
public string $ldhName,
public \DateTimeImmutable $updatedAt,
) {
}
}

View File

@ -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;
}
}

View File

@ -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;

View File

@ -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) {
/*

View File

@ -0,0 +1,43 @@
<?php
namespace App\Repository;
use App\Entity\DomainPurchase;
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
use Doctrine\Persistence\ManagerRegistry;
/**
* @extends ServiceEntityRepository<DomainPurchase>
*/
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()
// ;
// }
}

View File

@ -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);