<?php
declare(strict_types=1);
namespace App\Entity;
use DateTimeImmutable;
use DateTimeInterface;
use Doctrine\ORM\Mapping as ORM;
use DomainException;
/**
* Commande de paiement Stripe.
*
* L'entité porte les données métier structurées de fulfillment
* (`type`, `credits`, `price`, `demandeIds`) pour éviter un payload JSON polymorphe
* ainsi qu'un hash de token de retour pour les routes success/cancel stateless.
*/
#[ORM\Entity]
#[ORM\Table(name: 'orders')]
class Order
{
public const string STATUS_PENDING = 'pending';
public const string STATUS_PAID = 'paid';
public const string STATUS_FAILED = 'failed';
public const string STATUS_CANCELED = 'canceled';
public const string TYPE_PACK = 'pack';
public const string TYPE_DEMANDE = 'demande';
public const string TYPE_CART = 'cart';
#[ORM\Id]
#[ORM\GeneratedValue(strategy: 'AUTO')]
#[ORM\Column(type: 'integer')]
private ?int $id = null;
#[ORM\Column(name: 'created_at', type: 'datetime_immutable')]
private DateTimeImmutable $createdAt;
#[ORM\Column(name: 'checkout_session_id', type: 'string', length: 255, unique: true)]
private string $checkoutSessionId;
#[ORM\Column(name: 'checkout_expires_at', type: 'datetime_immutable', nullable: true)]
private ?DateTimeImmutable $checkoutExpiresAt = null;
#[ORM\Column(type: 'string', length: 20, options: ['default' => self::STATUS_PENDING])]
private string $status = self::STATUS_PENDING;
#[ORM\Column(name: 'stripe_event_id', type: 'string', length: 255, unique: true, nullable: true)]
private ?string $stripeEventId = null;
#[ORM\Column(name: 'order_type', type: 'string', length: 20, options: ['default' => self::TYPE_PACK])]
private string $type = self::TYPE_PACK;
#[ORM\Column(type: 'integer', nullable: true)]
private ?int $credits = null;
#[ORM\Column(type: 'integer', options: ['default' => 0])]
private int $price = 0;
#[ORM\Column(name: 'return_token_hash', type: 'string', length: 64, nullable: true)]
private ?string $returnTokenHash = null;
/**
* Snapshot immuable des identifiants de demandes associés à la commande.
*
* @var list<int>
*/
#[ORM\Column(name: 'demande_ids', type: 'json')]
private array $demandeIds = [];
#[ORM\ManyToOne(targetEntity: Prestataire::class)]
#[ORM\JoinColumn(name: 'prestataire_id', referencedColumnName: 'id', nullable: false)]
private Prestataire $prestataire;
public function __construct()
{
$this->createdAt = new DateTimeImmutable();
}
public function getId(): ?int
{
return $this->id;
}
public function getCreatedAt(): DateTimeImmutable
{
return $this->createdAt;
}
public function setCreatedAt(DateTimeImmutable $createdAt): self
{
$this->createdAt = $createdAt;
return $this;
}
public function getCheckoutSessionId(): string
{
return $this->checkoutSessionId;
}
public function setCheckoutSessionId(string $checkoutSessionId): self
{
$this->checkoutSessionId = $checkoutSessionId;
return $this;
}
public function getCheckoutExpiresAt(): ?DateTimeImmutable
{
return $this->checkoutExpiresAt;
}
public function setCheckoutExpiresAt(?DateTimeImmutable $checkoutExpiresAt): self
{
$this->checkoutExpiresAt = $checkoutExpiresAt;
return $this;
}
public function isCheckoutExpired(?DateTimeInterface $reference = null): bool
{
if (!$this->checkoutExpiresAt instanceof DateTimeImmutable) {
return false;
}
$effectiveReference = $reference ?? new DateTimeImmutable();
return $this->checkoutExpiresAt <= $effectiveReference;
}
public function getStatus(): string
{
return $this->status;
}
public function setStatus(string $status): self
{
$this->status = $status;
return $this;
}
public function getStripeEventId(): ?string
{
return $this->stripeEventId;
}
public function setStripeEventId(?string $stripeEventId): self
{
$this->stripeEventId = $stripeEventId;
return $this;
}
public function getType(): string
{
return $this->type;
}
/**
* @param string $type Type métier de la commande (`pack`, `demande`, `cart`).
*
* @throws DomainException Si le type n'est pas supporté.
*/
public function setType(string $type): self
{
if (!in_array($type, [self::TYPE_PACK, self::TYPE_DEMANDE, self::TYPE_CART], true)) {
throw new DomainException(sprintf('Unsupported order type "%s".', $type));
}
$this->type = $type;
return $this;
}
/**
* @return int|null Nombre de crédits achetés si type `pack`.
*/
public function getCredits(): ?int
{
return $this->credits;
}
/**
* @param int|null $credits Nombre de crédits achetés si type `pack`.
*/
public function setCredits(?int $credits): self
{
$this->credits = $credits;
return $this;
}
/**
* @return int Montant payé figé au moment de la transaction.
*/
public function getPrice(): int
{
return $this->price;
}
/**
* @param int $price Montant payé figé au moment de la transaction.
*
* @throws DomainException Si le montant n'est pas strictement positif.
*/
public function setPrice(int $price): self
{
if ($price <= 0) {
throw new DomainException(sprintf('Invalid order price "%d".', $price));
}
$this->price = $price;
return $this;
}
/**
* @return string|null Hash SHA-256 du token de retour success/cancel.
*/
public function getReturnTokenHash(): ?string
{
return $this->returnTokenHash;
}
/**
* @param string|null $returnTokenHash Hash SHA-256 du token de retour.
*/
public function setReturnTokenHash(?string $returnTokenHash): self
{
$this->returnTokenHash = $returnTokenHash;
return $this;
}
/**
* @return list<int>
*/
public function getDemandeIds(): array
{
return $this->demandeIds;
}
/**
* @param list<int> $demandeIds
*/
public function setDemandeIds(array $demandeIds): self
{
$normalizedIds = [];
foreach ($demandeIds as $demandeId) {
$normalizedId = (int) $demandeId;
if ($normalizedId <= 0) {
throw new DomainException(sprintf('Invalid demande id "%s".', (string) $demandeId));
}
$normalizedIds[$normalizedId] = $normalizedId;
}
$snapshot = array_values($normalizedIds);
if ($this->demandeIds !== [] && $this->demandeIds !== $snapshot) {
throw new DomainException('Order demande ids snapshot is immutable once initialized.');
}
$this->demandeIds = $snapshot;
return $this;
}
public function getPrestataire(): Prestataire
{
return $this->prestataire;
}
public function setPrestataire(Prestataire $prestataire): self
{
$this->prestataire = $prestataire;
return $this;
}
}