src/Entity/Reservation.php line 19
<?phpnamespace App\Entity;use App\Repository\ReservationRepository;use App\Traits\TimeStampTrait;use Doctrine\Common\Collections\ArrayCollection;use Doctrine\Common\Collections\Collection;use Doctrine\DBAL\Types\Types;use Doctrine\ORM\Mapping as ORM;use Symfony\Component\Validator\Constraints as Assert;#[ORM\Entity(repositoryClass: ReservationRepository::class)]#[ORM\Table(name: 'reservation')]#[ORM\Index(columns: ['status'], name: 'idx_res_status')]#[ORM\Index(columns: ['check_in_date', 'check_out_date'], name: 'idx_res_dates')]#[ORM\UniqueConstraint(name: 'ux_res_code', columns: ['code'])]#[ORM\HasLifecycleCallbacks]class Reservation{use TimeStampTrait; // createdAt, updatedAt si ton trait les gère// ---- Status constants ---------------------------------------------------public const STATUS_TENTATIVE = 'tentative';public const STATUS_CONFIRMED = 'confirmed';public const STATUS_ARRIVED = 'arrived';public const STATUS_DEPARTED = 'departed';public const STATUS_CANCELED = 'canceled';/*** Mapping for display (FR labels)*/public const STATUS_LABELS = [self::STATUS_TENTATIVE => 'Tentative',self::STATUS_CONFIRMED => 'Confirmée',self::STATUS_ARRIVED => 'Arrivée',self::STATUS_DEPARTED => 'Parti',self::STATUS_CANCELED => 'Annulée',];public const STATUS_BADGES = ['tentative' => 'bg-warning text-dark','confirmed' => 'bg-success','arrived' => 'bg-info','departed' => 'bg-secondary','canceled' => 'bg-danger',];// ---- Identity -----------------------------------------------------------#[ORM\Id]#[ORM\GeneratedValue]#[ORM\Column(type: Types::INTEGER)]private ?int $id = null;/** Human-friendly ref (e.g. RES-2025-000123) */#[ORM\Column(length: 40, nullable: true)]private ?string $code = null;// ---- Relations ----------------------------------------------------------#[ORM\ManyToOne(inversedBy: 'reservations')]#[ORM\JoinColumn(nullable: false, onDelete: 'RESTRICT')]#[Assert\NotNull(message: 'L’unité (habitat) est requise.')]private ?Habitat $habitat = null; // unité actuelle/*** Unité d’origine (mise à la création, non modifiée ensuite).* Permet de savoir d’où vient la réservation même après des transferts d’unité.*/#[ORM\ManyToOne]#[ORM\JoinColumn(nullable: true, onDelete: 'SET NULL')]private ?Habitat $originalHabitat = null;/** The guest/customer making the stay */#[ORM\ManyToOne]#[ORM\JoinColumn(nullable: false, onDelete: 'RESTRICT')]#[Assert\NotNull(message: 'Le client (locataire) est requis.')]private ?Locataire $locataire = null;/** Detailed charges (nights, cleaning, extras…) */#[ORM\OneToMany(mappedBy: 'reservation', targetEntity: ReservationCharge::class, cascade: ['persist', 'remove'], orphanRemoval: true)]private Collection $charges;// ---- Dates & people -----------------------------------------------------#[ORM\Column(type: Types::DATE_MUTABLE)]#[Assert\NotNull]private ?\DateTimeInterface $checkInDate = null;#[ORM\Column(type: Types::DATE_MUTABLE)]#[Assert\NotNull]private ?\DateTimeInterface $checkOutDate = null; // exclusive/** Derived (checkOut - checkIn); kept for reporting/perf */#[ORM\Column(type: Types::INTEGER)]#[Assert\Positive(message: 'Le nombre de nuits doit être > 0.')]private int $nights = 1;#[ORM\Column(type: Types::INTEGER, options: ['default' => 1])]#[Assert\Positive]private int $adults = 1;#[ORM\Column(type: Types::INTEGER, options: ['default' => 0])]#[Assert\GreaterThanOrEqual(0)]private int $children = 0;#[ORM\Column(length: 50, nullable: true)]private ?string $source = null; // walk-in, WhatsApp, Booking, Airbnb…// ---- Financials ---------------------------------------------------------#[ORM\Column(type: Types::STRING, length: 10, options: ['default' => 'USD'])]private string $currency = 'USD';/** Total (in cents) for the stay (sum of charges) */#[ORM\Column(type: Types::DECIMAL, precision: 10, scale: 2)]private string $totalAmount;/** Remaining balance (in cents), if you track it here */#[ORM\Column(type: Types::DECIMAL, precision: 10, scale: 2)]private string $balanceDue;// ---- Status & notes -----------------------------------------------------#[ORM\Column(length: 12, options: ['default' => self::STATUS_CONFIRMED])]#[Assert\Choice([self::STATUS_TENTATIVE,self::STATUS_CONFIRMED,self::STATUS_ARRIVED,self::STATUS_DEPARTED,self::STATUS_CANCELED])]private string $status = self::STATUS_CONFIRMED;#[ORM\Column(type: Types::TEXT, nullable: true)]private ?string $notes = null;#[ORM\ManyToOne(inversedBy: 'reservations')]private ?Entreprise $marchand = null;#[ORM\Column]private ?bool $isBooked = null;#[ORM\Column(length: 25, nullable: true)]private ?string $term = null; // night|week|month (ou variante)#[ORM\ManyToOne(inversedBy: 'reservations')]private ?User $createdBy = null;// ---- Unit move history --------------------------------------------------/*** Historique des mouvements d’unité (JSON array)* Chaque entrée :* {fromId,toId,fromName,toName,movedAt(ISO8601),movedBy,reason,priceDelta}*/#[ORM\Column(type: Types::JSON, nullable: true)]private ?array $unitHistory = [];#[ORM\Column(type: Types::DATETIME_IMMUTABLE, nullable: true)]private ?\DateTimeImmutable $lastMoveAt = null;#[ORM\Column(type: Types::DECIMAL, precision: 10, scale: 2, nullable: true)]private ?string $unitPrice = null;#[ORM\Column(nullable: true)]private ?int $quantity = null;#[ORM\Column(nullable: true)]private ?int $subtotal = null;#[ORM\Column(type: Types::DECIMAL, precision: 10, scale: 2, nullable: true)]private ?string $fees = null;#[ORM\Column(type: Types::DECIMAL, precision: 10, scale: 2, nullable: true)]private ?string $total = null;// ---- Constructor --------------------------------------------------------public function __construct(){$this->charges = new ArrayCollection();}// ---- Domain validations -------------------------------------------------/** Ensures check-in < check-out. */#[Assert\IsTrue(message: 'La date de départ doit être strictement postérieure à la date d’arrivée.')]public function isValidPeriod(): bool{if (!$this->checkInDate || !$this->checkOutDate) {return true; // autres contraintes valideront les nulls}return $this->checkOutDate > $this->checkInDate;}// ---- Lifecycle: compute nights & totals --------------------------------#[ORM\PrePersist]#[ORM\PreUpdate]public function computeDerivedFields(): void{if ($this->checkInDate && $this->checkOutDate) {$this->nights = (int) $this->checkInDate->diff($this->checkOutDate)->days;}// Si originalHabitat non défini à la création, l’initialiserif (($this->originalHabitat === null) && $this->habitat !== null && $this->id === null) {$this->originalHabitat = $this->habitat;}$this->recalculateTotals();}// ---- Helpers ------------------------------------------------------------/** Recalculate totalAmount from charges; balanceDue untouched here. */// In Reservation entity:public function recalculateTotals(): void{$total = 0.0;$paid = 0.0;foreach ($this->charges as $c) {$total += $c->getTotalAmount();$paid += $c->getPaidAmount();}$this->totalAmount = $total;$this->balanceDue = (max(0, $total - $paid));}public function addCharge(ReservationCharge $charge): self{if (!$this->charges->contains($charge)) {$this->charges->add($charge);$charge->setReservation($this);$this->recalculateTotals();}return $this;}public function removeCharge(ReservationCharge $charge): self{if ($this->charges->removeElement($charge)) {if ($charge->getReservation() === $this) {$charge->setReservation(null);}$this->recalculateTotals();}return $this;}/** Convenience: set both dates at once. */public function setPeriod(\DateTimeInterface $checkIn, \DateTimeInterface $checkOut): self{$this->checkInDate = $checkIn;$this->checkOutDate = $checkOut;return $this;}/** Overlap helper (repository/service devrait faire le contrôle centralisé). */public function overlapsWith(\DateTimeInterface $from, \DateTimeInterface $to): bool{if (!$this->checkInDate || !$this->checkOutDate) {return false;}// [checkIn, checkOut) overlaps [from, to)return $this->checkInDate < $to && $this->checkOutDate > $from && $this->status !== self::STATUS_CANCELED;}// ---- Unit move history helpers -----------------------------------------// --- in Reservation.php ---public function getUnitHistory(): array{return is_array($this->unitHistory) ? $this->unitHistory : [];}public function setUnitHistory(?array $history): self{$this->unitHistory = $history ?? [];return $this;}/*** Append one movement entry to unitHistory JSON.* $priceDelta = in your money unit (e.g. cents if you use integers; adapt if you use floats)*/public function addUnitHistoryEntry(?int $fromId,?string $fromName,?int $toId,?string $toName,?string $movedBy = null,?string $reason = null,int|float $priceDelta = 0,?\DateTimeInterface $movedAt = null): self {$h = $this->getUnitHistory();$h[] = ['fromId' => $fromId,'fromName' => $fromName,'toId' => $toId,'toName' => $toName,'movedAt' => ($movedAt ?? new \DateTimeImmutable())->format(\DateTimeInterface::ATOM), // ISO8601'movedBy' => $movedBy, // e.g. user email/name'reason' => $reason,'priceDelta' => $priceDelta,];$this->unitHistory = $h;return $this;}// ---- Getters / Setters --------------------------------------------------public function getId(): ?int { return $this->id; }public function getCode(): ?string { return $this->code; }public function setCode(?string $code): self { $this->code = $code; return $this; }public function getHabitat(): ?Habitat { return $this->habitat; }public function setHabitat(?Habitat $habitat): self { $this->habitat = $habitat; return $this; }public function getOriginalHabitat(): ?Habitat { return $this->originalHabitat; }public function setOriginalHabitat(?Habitat $h): self { $this->originalHabitat = $h; return $this; }/** @return Collection<int, ReservationCharge> */public function getCharges(): Collection { return $this->charges; }public function getLocataire(): ?Locataire { return $this->locataire; }public function setLocataire(?Locataire $locataire): self { $this->locataire = $locataire; return $this; }public function getCheckInDate(): ?\DateTimeInterface { return $this->checkInDate; }public function setCheckInDate(?\DateTimeInterface $d): self { $this->checkInDate = $d; return $this; }public function getCheckOutDate(): ?\DateTimeInterface { return $this->checkOutDate; }public function setCheckOutDate(?\DateTimeInterface $d): self { $this->checkOutDate = $d; return $this; }public function getNights(): int { return $this->nights; }public function setNights(int $n): self { $this->nights = $n; return $this; } // généralement calculépublic function getAdults(): int { return $this->adults; }public function setAdults(int $a): self { $this->adults = $a; return $this; }public function getChildren(): int { return $this->children; }public function setChildren(int $c): self { $this->children = $c; return $this; }public function getSource(): ?string { return $this->source; }public function setSource(?string $s): self { $this->source = $s; return $this; }public function getCurrency(): string { return $this->currency; }public function setCurrency(string $cur): self { $this->currency = $cur; return $this; }public function getTotalAmount(): int { return $this->totalAmount; }public function setTotalAmount(int $amt): self { $this->totalAmount = $amt; return $this; }public function getBalanceDue(): int { return $this->balanceDue; }public function setBalanceDue(int $amt): self { $this->balanceDue = $amt; return $this; }public function getStatus(): string { return $this->status; }public function setStatus(string $status): self { $this->status = $status; return $this; }public function getNotes(): ?string { return $this->notes; }public function setNotes(?string $n): self { $this->notes = $n; return $this; }public function getMarchand(): ?Entreprise { return $this->marchand; }public function setMarchand(?Entreprise $marchand): static { $this->marchand = $marchand; return $this; }public function isIsBooked(): ?bool { return $this->isBooked; }public function setIsBooked(bool $isBooked): static { $this->isBooked = $isBooked; return $this; }public function getTerm(): ?string { return $this->term; }public function setTerm(?string $term): static { $this->term = $term; return $this; }public function getCreatedBy(): ?User { return $this->createdBy; }public function setCreatedBy(?User $createdBy): static { $this->createdBy = $createdBy; return $this; }public function getLastMoveAt(): ?\DateTimeImmutable { return $this->lastMoveAt; }public function setLastMoveAt(?\DateTimeImmutable $d): self { $this->lastMoveAt = $d; return $this; }public function __toString(): string{$ref = $this->code ?? ('RES#'.$this->id);$h = $this->habitat?->getLibelle() ?? $this->habitat?->getCode() ?? 'Habitat';$in = $this->checkInDate?->format('Y-m-d') ?? '?';$out = $this->checkOutDate?->format('Y-m-d') ?? '?';return sprintf('%s – %s (%s → %s)', $ref, $h, $in, $out);}public function getUnitPrice(): ?string{return $this->unitPrice;}public function setUnitPrice(?string $unitPrice): static{$this->unitPrice = $unitPrice;return $this;}public function getQuantity(): ?int{return $this->quantity;}public function setQuantity(?int $quantity): static{$this->quantity = $quantity;return $this;}public function getSubtotal(): ?int{return $this->subtotal;}public function setSubtotal(?int $subtotal): static{$this->subtotal = $subtotal;return $this;}public function getFees(): ?string{return $this->fees;}public function setFees(?string $fees): static{$this->fees = $fees;return $this;}public function getTotal(): ?string{return $this->total;}public function setTotal(?string $total): static{$this->total = $total;return $this;}}