chore: add documentation

This commit is contained in:
Björn Fromme
2025-07-17 11:37:11 +02:00
parent 09438cfbd9
commit 7aef767619
2 changed files with 75 additions and 2 deletions
@@ -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
+32 -2
View File
@@ -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');