chore: add documentation
This commit is contained in:
@@ -8,11 +8,22 @@ use Symfony\Component\EventDispatcher\EventSubscriberInterface;
|
||||
use Symfony\Component\Workflow\Event\GuardEvent;
|
||||
use Symfony\Contracts\Translation\TranslatorInterface;
|
||||
|
||||
/**
|
||||
* Workflow guard subscriber for disposition document lifecycle management.
|
||||
* Enforces business rules and timing constraints for contract and invoice workflows,
|
||||
* ensuring documents are processed in the correct order and within valid time windows.
|
||||
*/
|
||||
class DispositionWorkflowGuardSubscriber implements EventSubscriberInterface
|
||||
{
|
||||
public function __construct(private readonly TranslatorInterface $translator)
|
||||
{}
|
||||
|
||||
/**
|
||||
* Registers guard events for disposition workflow transitions.
|
||||
* Guards control when workflow transitions can occur based on business rules.
|
||||
*
|
||||
* @return array<string, array{0: string}> Event name to method mappings
|
||||
*/
|
||||
public static function getSubscribedEvents(): array
|
||||
{
|
||||
return [
|
||||
@@ -23,12 +34,19 @@ class DispositionWorkflowGuardSubscriber implements EventSubscriberInterface
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Guards the contract confirmation transition.
|
||||
* Blocks transition if no contract document exists or if it hasn't been checked by admin.
|
||||
*
|
||||
* @param GuardEvent $event The workflow guard event
|
||||
*/
|
||||
public function guardConfirmContract(GuardEvent $event): void
|
||||
{
|
||||
/** @var Disposition $disposition */
|
||||
$disposition = $event->getSubject();
|
||||
$contractDocument = $disposition->getDocumentByType(Upload::TYPE_CONTRACT);
|
||||
|
||||
// Contract must exist and be in checked status to allow confirmation
|
||||
if (null === $contractDocument || Upload::STATUS_CHECKED !== $contractDocument->getStatus()) {
|
||||
$message = $this
|
||||
->translator
|
||||
@@ -38,6 +56,13 @@ class DispositionWorkflowGuardSubscriber implements EventSubscriberInterface
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Guards the contract upload transition.
|
||||
* Blocks upload if the deadline has passed (day before assignment starts).
|
||||
* Contracts must be uploaded within 7 days of disposition creation until assignment start.
|
||||
*
|
||||
* @param GuardEvent $event The workflow guard event
|
||||
*/
|
||||
public function guardUploadContract(GuardEvent $event): void
|
||||
{
|
||||
/** @var Disposition $disposition */
|
||||
@@ -49,6 +74,7 @@ class DispositionWorkflowGuardSubscriber implements EventSubscriberInterface
|
||||
$dueDateFrom = $disposition->getCreatedAt()->modify('+ 7days');
|
||||
$dueDateTo = $assignmentPeriod->end->modify('-1 day');
|
||||
|
||||
// Block upload if deadline has passed
|
||||
if ($dueDateTo < $today) {
|
||||
$message = $this
|
||||
->translator
|
||||
@@ -61,6 +87,14 @@ class DispositionWorkflowGuardSubscriber implements EventSubscriberInterface
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Guards the invoice upload transition.
|
||||
* Enforces two requirements:
|
||||
* 1. Contract must exist and be checked before invoice upload
|
||||
* 2. Invoice can only be uploaded after assignment end date
|
||||
*
|
||||
* @param GuardEvent $event The workflow guard event
|
||||
*/
|
||||
public function guardUploadInvoice(GuardEvent $event): void
|
||||
{
|
||||
/** @var Disposition $disposition */
|
||||
@@ -71,6 +105,7 @@ class DispositionWorkflowGuardSubscriber implements EventSubscriberInterface
|
||||
|
||||
$contractDocument = $disposition->getDocumentByType(Upload::TYPE_CONTRACT);
|
||||
|
||||
// Contract must be checked before invoice upload
|
||||
if (null === $contractDocument || Upload::STATUS_CHECKED !== $contractDocument->getStatus()) {
|
||||
$message = $this
|
||||
->translator
|
||||
@@ -81,6 +116,7 @@ class DispositionWorkflowGuardSubscriber implements EventSubscriberInterface
|
||||
|
||||
$earliestDate = $destination->getDateTo();
|
||||
|
||||
// Invoice can only be uploaded after assignment ends
|
||||
if ($earliestDate > $today) {
|
||||
$message = $this
|
||||
->translator
|
||||
@@ -92,12 +128,19 @@ class DispositionWorkflowGuardSubscriber implements EventSubscriberInterface
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Guards the invoice confirmation transition.
|
||||
* Blocks transition if no invoice document exists or if it hasn't been marked as paid.
|
||||
*
|
||||
* @param GuardEvent $event The workflow guard event
|
||||
*/
|
||||
public function guardConfirmInvoice(GuardEvent $event): void
|
||||
{
|
||||
/** @var Disposition $disposition */
|
||||
$disposition = $event->getSubject();
|
||||
$invoiceDocument = $disposition->getDocumentByType(Upload::TYPE_INVOICE);
|
||||
|
||||
// Invoice must exist and be in paid status to allow confirmation
|
||||
if (null === $invoiceDocument || Upload::STATUS_PAID !== $invoiceDocument->getStatus()) {
|
||||
$message = $this
|
||||
->translator
|
||||
|
||||
@@ -7,11 +7,20 @@ use App\Model\TimelineItem;
|
||||
use Carbon\CarbonPeriod;
|
||||
use function Symfony\Component\String\b;
|
||||
|
||||
/**
|
||||
* Service for processing assignments into timeline visualization data.
|
||||
* Organizes assignments and dispositions by hotel in a timeline format,
|
||||
* handling overlapping dates by distributing items across multiple rows.
|
||||
*/
|
||||
class TimelineService
|
||||
{
|
||||
/**
|
||||
* @param Assignment[] $assignments
|
||||
* @return array
|
||||
* Preprocesses assignments into a timeline structure organized by hotel.
|
||||
* Creates timeline items for both vacant assignment slots and actual dispositions,
|
||||
* distributing them across multiple rows to avoid date overlaps.
|
||||
*
|
||||
* @param Assignment[] $assignments Array of assignments to process
|
||||
* @return array Multi-dimensional array: [hotelCode][rowIndex][itemKey] => TimelineItem
|
||||
*/
|
||||
public function preprocess(array $assignments): array
|
||||
{
|
||||
@@ -34,18 +43,21 @@ class TimelineService
|
||||
$dispositionsCount = $assignment->getDispositions()->count();
|
||||
$dispositionsAvailable = $assignment->getAvailableDispositions();
|
||||
|
||||
// Handle assignments with no dispositions - create items for all available slots
|
||||
if (0 === $dispositionsCount) {
|
||||
$item = TimelineItem::fromAssignment($assignment);
|
||||
for ($i = 1; $i <= $assignment->getAvailableDispositions(); $i++) {
|
||||
$this->addItem($rows[$hotelCode], $item);
|
||||
}
|
||||
} else {
|
||||
// Create items for remaining vacant slots if any
|
||||
if ($dispositionsCount < $dispositionsAvailable) {
|
||||
$item = TimelineItem::fromAssignment($assignment);
|
||||
for ($i = 1; $i <= $dispositionsAvailable - $dispositionsCount; $i++) {
|
||||
$this->addItem($rows[$hotelCode], $item);
|
||||
}
|
||||
}
|
||||
// Create items for actual dispositions
|
||||
foreach ($assignment->getDispositions() as $disposition) {
|
||||
$item = TimelineItem::fromDisposition($disposition);
|
||||
$this->addItem($rows[$hotelCode], $item);
|
||||
@@ -56,15 +68,25 @@ class TimelineService
|
||||
return $rows;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a timeline item to the appropriate row, ensuring no date overlaps.
|
||||
* Finds the first available row where the item doesn't overlap with existing items,
|
||||
* or creates a new row if necessary.
|
||||
*
|
||||
* @param array $rows Reference to the rows array for a specific hotel
|
||||
* @param TimelineItem $item The timeline item to add
|
||||
*/
|
||||
private function addItem(array &$rows, TimelineItem $item): void
|
||||
{
|
||||
$rowIndex = 0;
|
||||
$fitsRow = false;
|
||||
|
||||
// Find first row where item doesn't overlap with existing items
|
||||
foreach ($rows as $index => $row) {
|
||||
$fitsRow = true;
|
||||
foreach ($row as $entry) {
|
||||
/** @var TimelineItem $entry */
|
||||
// Check for date overlap: item overlaps if its end >= entry start AND item start <= entry end
|
||||
if ($entry->getDateTo() >= $item->getDateFrom() && $entry->getDateFrom() <= $item->getDateTo()) {
|
||||
$fitsRow = false;
|
||||
}
|
||||
@@ -75,6 +97,7 @@ class TimelineService
|
||||
}
|
||||
}
|
||||
|
||||
// If no existing row fits, create a new row
|
||||
if (false === $fitsRow) {
|
||||
++$rowIndex;
|
||||
}
|
||||
@@ -82,6 +105,13 @@ class TimelineService
|
||||
$rows[$rowIndex][$item->getKey()] = $item;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a time window for timeline visualization.
|
||||
* Returns a period from 6 weeks ago to 6 months in the future,
|
||||
* providing context for both recent and upcoming assignments.
|
||||
*
|
||||
* @return CarbonPeriod The active time window for timeline display
|
||||
*/
|
||||
public function getActiveWindow(): CarbonPeriod
|
||||
{
|
||||
$dateFrom = (new \DateTimeImmutable())->modify('-6 weeks');
|
||||
|
||||
Reference in New Issue
Block a user