src/Entity/Reservation.php line 19

  1. <?php
  2. namespace App\Entity;
  3. use App\Repository\ReservationRepository;
  4. use App\Traits\TimeStampTrait;
  5. use Doctrine\Common\Collections\ArrayCollection;
  6. use Doctrine\Common\Collections\Collection;
  7. use Doctrine\DBAL\Types\Types;
  8. use Doctrine\ORM\Mapping as ORM;
  9. use Symfony\Component\Validator\Constraints as Assert;
  10. #[ORM\Entity(repositoryClassReservationRepository::class)]
  11. #[ORM\Table(name'reservation')]
  12. #[ORM\Index(columns: ['status'], name'idx_res_status')]
  13. #[ORM\Index(columns: ['check_in_date''check_out_date'], name'idx_res_dates')]
  14. #[ORM\UniqueConstraint(name'ux_res_code'columns: ['code'])]
  15. #[ORM\HasLifecycleCallbacks]
  16. class Reservation
  17. {
  18.     use TimeStampTrait// createdAt, updatedAt si ton trait les gère
  19.     // ---- Status constants ---------------------------------------------------
  20.     public const STATUS_TENTATIVE 'tentative';
  21.     public const STATUS_CONFIRMED 'confirmed';
  22.     public const STATUS_ARRIVED   'arrived';
  23.     public const STATUS_DEPARTED  'departed';
  24.     public const STATUS_CANCELED  'canceled';
  25.     /**
  26.      * Mapping for display (FR labels)
  27.      */
  28.     public const STATUS_LABELS = [
  29.         self::STATUS_TENTATIVE => 'Tentative',
  30.         self::STATUS_CONFIRMED => 'Confirmée',
  31.         self::STATUS_ARRIVED   => 'Arrivée',
  32.         self::STATUS_DEPARTED  => 'Parti',
  33.         self::STATUS_CANCELED  => 'Annulée',
  34.     ];
  35.     public const STATUS_BADGES = [
  36.         'tentative' => 'bg-warning text-dark',
  37.         'confirmed' => 'bg-success',
  38.         'arrived'   => 'bg-info',
  39.         'departed'  => 'bg-secondary',
  40.         'canceled'  => 'bg-danger',
  41.     ];
  42.     // ---- Identity -----------------------------------------------------------
  43.     #[ORM\Id]
  44.     #[ORM\GeneratedValue]
  45.     #[ORM\Column(typeTypes::INTEGER)]
  46.     private ?int $id null;
  47.     /** Human-friendly ref (e.g. RES-2025-000123) */
  48.     #[ORM\Column(length40nullabletrue)]
  49.     private ?string $code null;
  50.     // ---- Relations ----------------------------------------------------------
  51.     #[ORM\ManyToOne(inversedBy'reservations')]
  52.     #[ORM\JoinColumn(nullablefalseonDelete'RESTRICT')]
  53.     #[Assert\NotNull(message'L’unité (habitat) est requise.')]
  54.     private ?Habitat $habitat null// unité actuelle
  55.     /**
  56.      * Unité d’origine (mise à la création, non modifiée ensuite).
  57.      * Permet de savoir d’où vient la réservation même après des transferts d’unité.
  58.      */
  59.     #[ORM\ManyToOne]
  60.     #[ORM\JoinColumn(nullabletrueonDelete'SET NULL')]
  61.     private ?Habitat $originalHabitat null;
  62.     /** The guest/customer making the stay */
  63.     #[ORM\ManyToOne]
  64.     #[ORM\JoinColumn(nullablefalseonDelete'RESTRICT')]
  65.     #[Assert\NotNull(message'Le client (locataire) est requis.')]
  66.     private ?Locataire $locataire null;
  67.     /** Detailed charges (nights, cleaning, extras…) */
  68.     #[ORM\OneToMany(mappedBy'reservation'targetEntityReservationCharge::class, cascade: ['persist''remove'], orphanRemovaltrue)]
  69.     private Collection $charges;
  70.     // ---- Dates & people -----------------------------------------------------
  71.     #[ORM\Column(typeTypes::DATE_MUTABLE)]
  72.     #[Assert\NotNull]
  73.     private ?\DateTimeInterface $checkInDate null;
  74.     #[ORM\Column(typeTypes::DATE_MUTABLE)]
  75.     #[Assert\NotNull]
  76.     private ?\DateTimeInterface $checkOutDate null// exclusive
  77.     /** Derived (checkOut - checkIn); kept for reporting/perf */
  78.     #[ORM\Column(typeTypes::INTEGER)]
  79.     #[Assert\Positive(message'Le nombre de nuits doit être > 0.')]
  80.     private int $nights 1;
  81.     #[ORM\Column(typeTypes::INTEGERoptions: ['default' => 1])]
  82.     #[Assert\Positive]
  83.     private int $adults 1;
  84.     #[ORM\Column(typeTypes::INTEGERoptions: ['default' => 0])]
  85.     #[Assert\GreaterThanOrEqual(0)]
  86.     private int $children 0;
  87.     #[ORM\Column(length50nullabletrue)]
  88.     private ?string $source null// walk-in, WhatsApp, Booking, Airbnb…
  89.     // ---- Financials ---------------------------------------------------------
  90.     #[ORM\Column(typeTypes::STRINGlength10options: ['default' => 'USD'])]
  91.     private string $currency 'USD';
  92.     /** Total (in cents) for the stay (sum of charges) */
  93.     #[ORM\Column(typeTypes::DECIMALprecision10scale2)]
  94.     private string $totalAmount;
  95.     /** Remaining balance (in cents), if you track it here */
  96.     #[ORM\Column(typeTypes::DECIMALprecision10scale2)]
  97.     private string $balanceDue;
  98.     // ---- Status & notes -----------------------------------------------------
  99.     #[ORM\Column(length12options: ['default' => self::STATUS_CONFIRMED])]
  100.     #[Assert\Choice([
  101.         self::STATUS_TENTATIVE,
  102.         self::STATUS_CONFIRMED,
  103.         self::STATUS_ARRIVED,
  104.         self::STATUS_DEPARTED,
  105.         self::STATUS_CANCELED
  106.     ])]
  107.     private string $status self::STATUS_CONFIRMED;
  108.     #[ORM\Column(typeTypes::TEXTnullabletrue)]
  109.     private ?string $notes null;
  110.     #[ORM\ManyToOne(inversedBy'reservations')]
  111.     private ?Entreprise $marchand null;
  112.     #[ORM\Column]
  113.     private ?bool $isBooked null;
  114.     #[ORM\Column(length25nullabletrue)]
  115.     private ?string $term null// night|week|month (ou variante)
  116.     #[ORM\ManyToOne(inversedBy'reservations')]
  117.     private ?User $createdBy null;
  118.     // ---- Unit move history --------------------------------------------------
  119.     /**
  120.      * Historique des mouvements d’unité (JSON array)
  121.      * Chaque entrée :
  122.      * {fromId,toId,fromName,toName,movedAt(ISO8601),movedBy,reason,priceDelta}
  123.      */
  124.     #[ORM\Column(typeTypes::JSONnullabletrue)]
  125.     private ?array $unitHistory = [];
  126.     #[ORM\Column(typeTypes::DATETIME_IMMUTABLEnullabletrue)]
  127.     private ?\DateTimeImmutable $lastMoveAt null;
  128.     #[ORM\Column(typeTypes::DECIMALprecision10scale2nullabletrue)]
  129.     private ?string $unitPrice null;
  130.     #[ORM\Column(nullabletrue)]
  131.     private ?int $quantity null;
  132.     #[ORM\Column(nullabletrue)]
  133.     private ?int $subtotal null;
  134.     #[ORM\Column(typeTypes::DECIMALprecision10scale2nullabletrue)]
  135.     private ?string $fees null;
  136.     #[ORM\Column(typeTypes::DECIMALprecision10scale2nullabletrue)]
  137.     private ?string $total null;
  138.     // ---- Constructor --------------------------------------------------------
  139.     public function __construct()
  140.     {
  141.         $this->charges = new ArrayCollection();
  142.     }
  143.     // ---- Domain validations -------------------------------------------------
  144.     /** Ensures check-in < check-out. */
  145.     #[Assert\IsTrue(message'La date de départ doit être strictement postérieure à la date d’arrivée.')]
  146.     public function isValidPeriod(): bool
  147.     {
  148.         if (!$this->checkInDate || !$this->checkOutDate) {
  149.             return true// autres contraintes valideront les nulls
  150.         }
  151.         return $this->checkOutDate $this->checkInDate;
  152.     }
  153.     // ---- Lifecycle: compute nights & totals --------------------------------
  154.     #[ORM\PrePersist]
  155.     #[ORM\PreUpdate]
  156.     public function computeDerivedFields(): void
  157.     {
  158.         if ($this->checkInDate && $this->checkOutDate) {
  159.             $this->nights = (int) $this->checkInDate->diff($this->checkOutDate)->days;
  160.         }
  161.         // Si originalHabitat non défini à la création, l’initialiser
  162.         if (($this->originalHabitat === null) && $this->habitat !== null && $this->id === null) {
  163.             $this->originalHabitat $this->habitat;
  164.         }
  165.         $this->recalculateTotals();
  166.     }
  167.     // ---- Helpers ------------------------------------------------------------
  168.     /** Recalculate totalAmount from charges; balanceDue untouched here. */
  169.     // In Reservation entity:
  170.     public function recalculateTotals(): void
  171.     {
  172.         $total 0.0;
  173.         $paid  0.0;
  174.         foreach ($this->charges as $c) {
  175.             $total += $c->getTotalAmount();
  176.             $paid  += $c->getPaidAmount();
  177.         }
  178.         $this->totalAmount $total;
  179.         $this->balanceDue  = (max(0$total $paid));
  180.     }
  181.     public function addCharge(ReservationCharge $charge): self
  182.     {
  183.         if (!$this->charges->contains($charge)) {
  184.             $this->charges->add($charge);
  185.             $charge->setReservation($this);
  186.             $this->recalculateTotals();
  187.         }
  188.         return $this;
  189.     }
  190.     public function removeCharge(ReservationCharge $charge): self
  191.     {
  192.         if ($this->charges->removeElement($charge)) {
  193.             if ($charge->getReservation() === $this) {
  194.                 $charge->setReservation(null);
  195.             }
  196.             $this->recalculateTotals();
  197.         }
  198.         return $this;
  199.     }
  200.     /** Convenience: set both dates at once. */
  201.     public function setPeriod(\DateTimeInterface $checkIn\DateTimeInterface $checkOut): self
  202.     {
  203.         $this->checkInDate $checkIn;
  204.         $this->checkOutDate $checkOut;
  205.         return $this;
  206.     }
  207.     /** Overlap helper (repository/service devrait faire le contrôle centralisé). */
  208.     public function overlapsWith(\DateTimeInterface $from\DateTimeInterface $to): bool
  209.     {
  210.         if (!$this->checkInDate || !$this->checkOutDate) {
  211.             return false;
  212.         }
  213.         // [checkIn, checkOut) overlaps [from, to)
  214.         return $this->checkInDate $to && $this->checkOutDate $from && $this->status !== self::STATUS_CANCELED;
  215.     }
  216.     // ---- Unit move history helpers -----------------------------------------
  217.     // --- in Reservation.php ---
  218.     public function getUnitHistory(): array
  219.     {
  220.         return is_array($this->unitHistory) ? $this->unitHistory : [];
  221.     }
  222.     public function setUnitHistory(?array $history): self
  223.     {
  224.         $this->unitHistory $history ?? [];
  225.         return $this;
  226.     }
  227.     /**
  228.      * Append one movement entry to unitHistory JSON.
  229.      * $priceDelta = in your money unit (e.g. cents if you use integers; adapt if you use floats)
  230.      */
  231.     public function addUnitHistoryEntry(
  232.         ?int    $fromId,
  233.         ?string $fromName,
  234.         ?int    $toId,
  235.         ?string $toName,
  236.         ?string $movedBy null,
  237.         ?string $reason  null,
  238.         int|float $priceDelta 0,
  239.         ?\DateTimeInterface $movedAt null
  240.     ): self {
  241.         $h $this->getUnitHistory();
  242.         $h[] = [
  243.             'fromId'     => $fromId,
  244.             'fromName'   => $fromName,
  245.             'toId'       => $toId,
  246.             'toName'     => $toName,
  247.             'movedAt'    => ($movedAt ?? new \DateTimeImmutable())->format(\DateTimeInterface::ATOM), // ISO8601
  248.             'movedBy'    => $movedBy,     // e.g. user email/name
  249.             'reason'     => $reason,
  250.             'priceDelta' => $priceDelta,
  251.         ];
  252.         $this->unitHistory $h;
  253.         return $this;
  254.     }
  255.     // ---- Getters / Setters --------------------------------------------------
  256.     public function getId(): ?int { return $this->id; }
  257.     public function getCode(): ?string { return $this->code; }
  258.     public function setCode(?string $code): self $this->code $code; return $this; }
  259.     public function getHabitat(): ?Habitat { return $this->habitat; }
  260.     public function setHabitat(?Habitat $habitat): self $this->habitat $habitat; return $this; }
  261.     public function getOriginalHabitat(): ?Habitat { return $this->originalHabitat; }
  262.     public function setOriginalHabitat(?Habitat $h): self $this->originalHabitat $h; return $this; }
  263.     /** @return Collection<int, ReservationCharge> */
  264.     public function getCharges(): Collection { return $this->charges; }
  265.     public function getLocataire(): ?Locataire { return $this->locataire; }
  266.     public function setLocataire(?Locataire $locataire): self $this->locataire $locataire; return $this; }
  267.     public function getCheckInDate(): ?\DateTimeInterface { return $this->checkInDate; }
  268.     public function setCheckInDate(?\DateTimeInterface $d): self $this->checkInDate $d; return $this; }
  269.     public function getCheckOutDate(): ?\DateTimeInterface { return $this->checkOutDate; }
  270.     public function setCheckOutDate(?\DateTimeInterface $d): self $this->checkOutDate $d; return $this; }
  271.     public function getNights(): int { return $this->nights; }
  272.     public function setNights(int $n): self $this->nights $n; return $this; } // généralement calculé
  273.     public function getAdults(): int { return $this->adults; }
  274.     public function setAdults(int $a): self $this->adults $a; return $this; }
  275.     public function getChildren(): int { return $this->children; }
  276.     public function setChildren(int $c): self $this->children $c; return $this; }
  277.     public function getSource(): ?string { return $this->source; }
  278.     public function setSource(?string $s): self $this->source $s; return $this; }
  279.     public function getCurrency(): string { return $this->currency; }
  280.     public function setCurrency(string $cur): self $this->currency $cur; return $this; }
  281.     public function getTotalAmount(): int { return $this->totalAmount; }
  282.     public function setTotalAmount(int $amt): self $this->totalAmount $amt; return $this; }
  283.     public function getBalanceDue(): int { return $this->balanceDue; }
  284.     public function setBalanceDue(int $amt): self $this->balanceDue $amt; return $this; }
  285.     public function getStatus(): string { return $this->status; }
  286.     public function setStatus(string $status): self $this->status $status; return $this; }
  287.     public function getNotes(): ?string { return $this->notes; }
  288.     public function setNotes(?string $n): self $this->notes $n; return $this; }
  289.     public function getMarchand(): ?Entreprise { return $this->marchand; }
  290.     public function setMarchand(?Entreprise $marchand): static { $this->marchand $marchand; return $this; }
  291.     public function isIsBooked(): ?bool { return $this->isBooked; }
  292.     public function setIsBooked(bool $isBooked): static { $this->isBooked $isBooked; return $this; }
  293.     public function getTerm(): ?string { return $this->term; }
  294.     public function setTerm(?string $term): static { $this->term $term; return $this; }
  295.     public function getCreatedBy(): ?User { return $this->createdBy; }
  296.     public function setCreatedBy(?User $createdBy): static { $this->createdBy $createdBy; return $this; }
  297.     public function getLastMoveAt(): ?\DateTimeImmutable { return $this->lastMoveAt; }
  298.     public function setLastMoveAt(?\DateTimeImmutable $d): self $this->lastMoveAt $d; return $this; }
  299.     public function __toString(): string
  300.     {
  301.         $ref $this->code ?? ('RES#'.$this->id);
  302.         $h   $this->habitat?->getLibelle() ?? $this->habitat?->getCode() ?? 'Habitat';
  303.         $in  $this->checkInDate?->format('Y-m-d') ?? '?';
  304.         $out $this->checkOutDate?->format('Y-m-d') ?? '?';
  305.         return sprintf('%s – %s (%s → %s)'$ref$h$in$out);
  306.     }
  307.     public function getUnitPrice(): ?string
  308.     {
  309.         return $this->unitPrice;
  310.     }
  311.     public function setUnitPrice(?string $unitPrice): static
  312.     {
  313.         $this->unitPrice $unitPrice;
  314.         return $this;
  315.     }
  316.     public function getQuantity(): ?int
  317.     {
  318.         return $this->quantity;
  319.     }
  320.     public function setQuantity(?int $quantity): static
  321.     {
  322.         $this->quantity $quantity;
  323.         return $this;
  324.     }
  325.     public function getSubtotal(): ?int
  326.     {
  327.         return $this->subtotal;
  328.     }
  329.     public function setSubtotal(?int $subtotal): static
  330.     {
  331.         $this->subtotal $subtotal;
  332.         return $this;
  333.     }
  334.     public function getFees(): ?string
  335.     {
  336.         return $this->fees;
  337.     }
  338.     public function setFees(?string $fees): static
  339.     {
  340.         $this->fees $fees;
  341.         return $this;
  342.     }
  343.     public function getTotal(): ?string
  344.     {
  345.         return $this->total;
  346.     }
  347.     public function setTotal(?string $total): static
  348.     {
  349.         $this->total $total;
  350.         return $this;
  351.     }
  352. }