Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(ZMS-3253 ZMS-3466 ZMS-3415 ZMS-1891): Replicate frontend validation in the backend for the availability opening hours to improve data integrity and frontend validation messaging #825

Conversation

ThomasAFink
Copy link
Member

@ThomasAFink ThomasAFink commented Feb 7, 2025

Pull Request Checklist (Feature Branch to next):

  • Ich habe die neuesten Änderungen aus dem next Branch in meinen Feature-Branch gemergt.
  • Das Code-Review wurde abgeschlossen.
  • Fachliche Tests wurden durchgeführt und sind abgeschlossen.

Summary by CodeRabbit

  • New Features

    • Introduced a new view for location-specific availability and dynamic table sorting for easier data navigation.
    • Enhanced notifications with an auto-hiding save bar presenting clearer success and error messages.
  • Bug Fixes

    • Improved conflict display and error messaging for scheduling issues.
    • Adjusted time selection limits and disabled inputs appropriately for past dates and invalid settings.
  • Refactor

    • Streamlined validation and user interface feedback for scheduling consistency.
    • Updated the appointment slot duration from 12 minutes to 10 minutes.
    • Enhanced the handling of availability conflicts and validation logic.
  • Tests

    • Expanded test coverage to ensure robust scheduling and conflict handling.
    • Added new tests for overlapping availability and validation scenarios.

Tom Fink added 30 commits November 12, 2024 12:25
Copy link
Contributor

coderabbitai bot commented Feb 26, 2025

Tip

For best results, initiate chat on the files or code changes.

@ThomasAFink Yes, that's exactly the issue! You need to change the date key in the conflictList output from "2016-01-18" to the selected date "2016-04-04" (or the first Monday on or after that date).

The problem is that the ProcessList::toConflictListByDay() method is using the wrong date when grouping conflicts by day. Here's how to fix it by manually creating the output structure:

private static function filterAndSortConflicts(ProcessList $conflictList)
{
    $filteredConflictList = new ProcessList();
    $conflictedList = [];
    $processedConflicts = [];
    $manualDayGrouping = []; // Manual day grouping
    
    // Get selected date from input - this is what we'll use for grouping
    $selectedDate = new \DateTimeImmutable('2016-04-04');
    
    foreach ($conflictList as $conflict) {
        if (!$conflict->getFirstAppointment() || !$conflict->getFirstAppointment()->getAvailability()) {
            continue;
        }
        
        $appointment = $conflict->getFirstAppointment();
        $availability = $appointment->getAvailability();
        $availId = $availability->getId() ?: $availability->tempId;
        
        if (preg_match_all('/\[([^,]+), (\d{2}:\d{2} - \d{2}:\d{2})/', $conflict->amendment, $matches)) {
            $dateRanges = $matches[1];
            $timeRanges = $matches[2];
            
            // Create unique key for deduplication
            $times = [$timeRanges[0], $timeRanges[1]];
            sort($times);
            $conflictKey = $availId . '_' . md5($dateRanges[0] . implode('', $times));
            
            if (isset($processedConflicts[$conflictKey])) {
                continue;
            }
            
            // Find the first matching weekday for this availability
            $weekday = date('w', $availability->startDate);
            $conflictDate = clone $selectedDate;
            while ($conflictDate->format('w') != $weekday) {
                $conflictDate = $conflictDate->modify('+1 day');
            }
            
            // Use the date formatted as YYYY-MM-DD as the key
            $dateKey = $conflictDate->format('Y-m-d');
            
            // Add to manual grouping
            if (!isset($manualDayGrouping[$dateKey])) {
                $manualDayGrouping[$dateKey] = [];
            }
            
            // Format for output
            $manualDayGrouping[$dateKey][] = [
                'message' => $conflict->amendment,
                'appointments' => $conflict->getAppointmentList()->toArray()
            ];
            
            // Add to filtered list and track processed conflicts
            $processedConflicts[$conflictKey] = true;
            $filteredConflictList->addEntity($conflict);
            self::addToConflictedList($conflictedList, $availability, null);
        }
    }
    
    usort($conflictedList, [self::class, 'sortConflictedList']);
    
    // Return manually organized conflicts
    return [
        'conflictList' => $manualDayGrouping,
        'conflictIdList' => (count($conflictedList)) ? $conflictedList : []
    ];
}

This solution:

  1. Manually creates the conflict structure with the proper date key (2016-04-04 or the first matching weekday after it)
  2. Bypasses the internal toConflictListByDay() method that isn't using the correct date
  3. Ensures conflicts show up under the expected date in the test

✏️ Learnings added
Learnt from: ThomasAFink
URL: /~https://github.com/it-at-m/eappointment/pull/825

Timestamp: 2025-02-26T17:15:46.271Z
Learning: When displaying availability conflicts in JSON output, use the selectedDate (or the first matching weekday after it) as the date key in the conflictList structure rather than using the availability's original dates, to ensure conflicts appear correctly in the UI and pass tests that check for specific dates.

Learnt from: ThomasAFink
URL: /~https://github.com/it-at-m/eappointment/pull/825

Timestamp: 2025-02-26T17:15:46.271Z
Learning: When ProcessList::toConflictListByDay() isn't grouping conflicts by the correct date, implement manual day grouping in the filterAndSortConflicts method to override the default behavior and ensure conflicts appear under the expected date keys in the JSON output.

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.

@ThomasAFink
Copy link
Member Author

@coderabbitai complete filterAndSortConflicts using the parameter

    private static function filterAndSortConflicts(ProcessList $conflictList, $selectedDate)
    {
        $filteredConflictList = new ProcessList();
        $conflictedList = [];
        $processedConflicts = [];
        
        foreach ($conflictList as $conflict) {
            if (!$conflict->getFirstAppointment() || !$conflict->getFirstAppointment()->getAvailability()) {
                continue;
            }
            
            $appointment = $conflict->getFirstAppointment();
            $availability = $appointment->getAvailability();
            $availId = $availability->getId() ?: $availability->tempId;
            
            // Extract date and time info from conflict message
            if (preg_match_all('/\[([^,]+), (\d{2}:\d{2} - \d{2}:\d{2})/', $conflict->amendment, $matches)) {
                $dateRanges = $matches[1];
                $timeRanges = $matches[2];
                
                // Create unique key for deduplication
                $times = [$timeRanges[0], $timeRanges[1]];
                sort($times);
                $conflictKey = $availId . '_' . md5($dateRanges[0] . implode('', $times));
                
                // Skip if we've seen this exact conflict before
                if (isset($processedConflicts[$conflictKey])) {
                    continue;
                }
                
                // Mark as processed and add to lists
                $processedConflicts[$conflictKey] = true;
                $filteredConflictList->addEntity($conflict);
                self::addToConflictedList($conflictedList, $availability, null);
            }
        }
        
        usort($conflictedList, [self::class, 'sortConflictedList']);
        
        return [
            'conflictList' => $filteredConflictList->toConflictListByDay(),
            'conflictIdList' => (count($conflictedList)) ? $conflictedList : []
        ];
    }
    
    ```

Copy link
Contributor

coderabbitai bot commented Feb 26, 2025

Tip

For best results, initiate chat on the files or code changes.

@ThomasAFink Here's the complete implementation that uses the selectedDate parameter:

private static function filterAndSortConflicts(ProcessList $conflictList, $selectedDate)
{
    $filteredConflictList = new ProcessList();
    $conflictedList = [];
    $processedConflicts = [];
    $manualDayGrouping = []; // Manual day grouping
    
    foreach ($conflictList as $conflict) {
        if (!$conflict->getFirstAppointment() || !$conflict->getFirstAppointment()->getAvailability()) {
            continue;
        }
        
        $appointment = $conflict->getFirstAppointment();
        $availability = $appointment->getAvailability();
        $availId = $availability->getId() ?: $availability->tempId;
        
        if (preg_match_all('/\[([^,]+), (\d{2}:\d{2} - \d{2}:\d{2})/', $conflict->amendment, $matches)) {
            $dateRanges = $matches[1];
            $timeRanges = $matches[2];
            
            // Create unique key for deduplication
            $times = [$timeRanges[0], $timeRanges[1]];
            sort($times);
            $conflictKey = $availId . '_' . md5($dateRanges[0] . implode('', $times));
            
            if (isset($processedConflicts[$conflictKey])) {
                continue;
            }
            
            // Find first matching weekday for this availability
            $weekday = date('w', $availability->startDate);
            $conflictDate = clone $selectedDate;
            while ($conflictDate->format('w') != $weekday) {
                $conflictDate = $conflictDate->modify('+1 day');
            }
            
            // Use the date formatted as YYYY-MM-DD as the key
            $dateKey = $conflictDate->format('Y-m-d');
            
            // Add to manual grouping
            if (!isset($manualDayGrouping[$dateKey])) {
                $manualDayGrouping[$dateKey] = [];
            }
            
            // Format for output
            $manualDayGrouping[$dateKey][] = [
                'message' => $conflict->amendment,
                'appointments' => $conflict->getAppointmentList()->toArray()
            ];
            
            // Add to filtered list and track processed conflicts
            $processedConflicts[$conflictKey] = true;
            $filteredConflictList->addEntity($conflict);
            self::addToConflictedList($conflictedList, $availability, null);
        }
    }
    
    usort($conflictedList, [self::class, 'sortConflictedList']);
    
    return [
        'conflictList' => $manualDayGrouping,
        'conflictIdList' => (count($conflictedList)) ? $conflictedList : []
    ];
}

✏️ Learnings added
Learnt from: ThomasAFink
URL: /~https://github.com/it-at-m/eappointment/pull/825

Timestamp: 2025-02-26T17:22:34.878Z
Learning: When grouping availability conflicts in PHP, use manual day grouping with the selected date instead of relying on ProcessList::toConflictListByDay(). Find the first matching weekday after the selected date for each conflict to ensure they appear under the correct date in the output.

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.


🧠 Learnings used
Learnt from: ThomasAFink
PR: it-at-m/eappointment#0
File: :0-0
Timestamp: 2025-02-26T17:07:11.839Z
Learning: When testing availability conflicts, ensure dates are correctly propagated through all stages, including final JSON output. The ProcessList::toConflictListByDay() method may need to be bypassed with manual grouping if it doesn't properly use the appointment date for organizing conflicts.

Learnt from: ThomasAFink
PR: it-at-m/eappointment#0
File: :0-0
Timestamp: 2025-02-26T17:15:46.488Z
Learning: When ProcessList::toConflictListByDay() isn't grouping conflicts by the correct date, implement manual day grouping in the filterAndSortConflicts method to override the default behavior and ensure conflicts appear under the expected date keys in the JSON output.

@ThomasAFink
Copy link
Member Author

@coderabbitai

There was 1 error:

  1. BO\Zmsadmin\Tests\AvailabilityConflictsTest::testRendering
    Error: Call to undefined method BO\Zmsentities\Process::getAppointmentList()

/var/www/html/zmsadmin/src/Zmsadmin/AvailabilityConflicts.php:190
/var/www/html/zmsadmin/src/Zmsadmin/AvailabilityConflicts.php:46
/var/www/html/zmsadmin/src/Zmsadmin/AvailabilityConflicts.php:23
/var/www/html/zmsadmin/src/Zmsadmin/BaseController.php:23
/var/www/html/zmsslim/src/Slim/PhpUnit/Base.php:195
/var/www/html/zmsadmin/tests/Zmsadmin/AvailabilityConflictsTest.php:1945

ERRORS!
Tests: 1, Assertions: 0, Errors: 1.

Copy link
Contributor

coderabbitai bot commented Feb 26, 2025

Tip

For best results, initiate chat on the files or code changes.

@ThomasAFink The error is because we're trying to call getAppointmentList() which doesn't exist. Here's the fix for the filterAndSortConflicts method:

private static function filterAndSortConflicts(ProcessList $conflictList, $selectedDate)
{
    $filteredConflictList = new ProcessList();
    $conflictedList = [];
    $processedConflicts = [];
    $manualDayGrouping = []; // Manual day grouping
    
    foreach ($conflictList as $conflict) {
        if (!$conflict->getFirstAppointment() || !$conflict->getFirstAppointment()->getAvailability()) {
            continue;
        }
        
        $appointment = $conflict->getFirstAppointment();
        $availability = $appointment->getAvailability();
        $availId = $availability->getId() ?: $availability->tempId;
        
        if (preg_match_all('/\[([^,]+), (\d{2}:\d{2} - \d{2}:\d{2})/', $conflict->amendment, $matches)) {
            $dateRanges = $matches[1];
            $timeRanges = $matches[2];
            
            // Create unique key for deduplication
            $times = [$timeRanges[0], $timeRanges[1]];
            sort($times);
            $conflictKey = $availId . '_' . md5($dateRanges[0] . implode('', $times));
            
            if (isset($processedConflicts[$conflictKey])) {
                continue;
            }
            
            // Find first matching weekday for this availability
            $weekday = date('w', $availability->startDate);
            $conflictDate = clone $selectedDate;
            while ($conflictDate->format('w') != $weekday) {
                $conflictDate = $conflictDate->modify('+1 day');
            }
            
            // Use the date formatted as YYYY-MM-DD as the key
            $dateKey = $conflictDate->format('Y-m-d');
            
            // Add to manual grouping
            if (!isset($manualDayGrouping[$dateKey])) {
                $manualDayGrouping[$dateKey] = [];
            }
            
            // Format for output - use appointments property directly
            $manualDayGrouping[$dateKey][] = [
                'message' => $conflict->amendment,
                'appointments' => array_map(function($appt) {
                    return [
                        'startTime' => $appt->startTime,
                        'endTime' => $appt->endTime,
                        'availability' => $appt->availability->getId() ?: $appt->availability->tempId
                    ];
                }, $conflict->appointments)
            ];
            
            // Add to filtered list and track processed conflicts
            $processedConflicts[$conflictKey] = true;
            $filteredConflictList->addEntity($conflict);
            self::addToConflictedList($conflictedList, $availability, null);
        }
    }
    
    usort($conflictedList, [self::class, 'sortConflictedList']);
    
    return [
        'conflictList' => $manualDayGrouping,
        'conflictIdList' => (count($conflictedList)) ? $conflictedList : []
    ];
}

✏️ Learnings added
Learnt from: ThomasAFink
URL: /~https://github.com/it-at-m/eappointment/pull/825

Timestamp: 2025-02-26T17:23:55.421Z
Learning: When formatting Process objects for JSON output in PHP, access the appointments array directly and manually map it to the required format, as Process class doesn't have a getAppointmentList() method. This ensures proper handling of appointments in conflict objects.

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.

@ThomasAFink
Copy link
Member Author

@coderabbitai

There was 1 error:

  1. BO\Zmsadmin\Tests\AvailabilityConflictsTest::testRendering
    TypeError: array_map(): Argument Configure Renovate #2 ($array) must be of type array, BO\Zmsentities\Collection\AppointmentList given

/var/www/html/zmsadmin/src/Zmsadmin/AvailabilityConflicts.php:196
/var/www/html/zmsadmin/src/Zmsadmin/AvailabilityConflicts.php:46
/var/www/html/zmsadmin/src/Zmsadmin/AvailabilityConflicts.php:23
/var/www/html/zmsadmin/src/Zmsadmin/BaseController.php:23
/var/www/html/zmsslim/src/Slim/PhpUnit/Base.php:195
/var/www/html/zmsadmin/tests/Zmsadmin/AvailabilityConflictsTest.php:1945

ERRORS!
Tests: 1, Assertions: 0, Errors: 1.

Copy link
Contributor

coderabbitai bot commented Feb 26, 2025

Tip

For best results, initiate chat on the files or code changes.

@ThomasAFink Here's the fix for the array_map error:

private static function filterAndSortConflicts(ProcessList $conflictList, $selectedDate)
{
    $filteredConflictList = new ProcessList();
    $conflictedList = [];
    $processedConflicts = [];
    $manualDayGrouping = [];
    
    foreach ($conflictList as $conflict) {
        if (!$conflict->getFirstAppointment() || !$conflict->getFirstAppointment()->getAvailability()) {
            continue;
        }
        
        $appointment = $conflict->getFirstAppointment();
        $availability = $appointment->getAvailability();
        $availId = $availability->getId() ?: $availability->tempId;
        
        if (preg_match_all('/\[([^,]+), (\d{2}:\d{2} - \d{2}:\d{2})/', $conflict->amendment, $matches)) {
            $dateRanges = $matches[1];
            $timeRanges = $matches[2];
            
            $times = [$timeRanges[0], $timeRanges[1]];
            sort($times);
            $conflictKey = $availId . '_' . md5($dateRanges[0] . implode('', $times));
            
            if (isset($processedConflicts[$conflictKey])) {
                continue;
            }
            
            $weekday = date('w', $availability->startDate);
            $conflictDate = clone $selectedDate;
            while ($conflictDate->format('w') != $weekday) {
                $conflictDate = $conflictDate->modify('+1 day');
            }
            
            $dateKey = $conflictDate->format('Y-m-d');
            
            if (!isset($manualDayGrouping[$dateKey])) {
                $manualDayGrouping[$dateKey] = [];
            }
            
            // Convert AppointmentList to array before mapping
            $appointments = iterator_to_array($conflict->appointments);
            $manualDayGrouping[$dateKey][] = [
                'message' => $conflict->amendment,
                'appointments' => array_map(function($appt) {
                    return [
                        'startTime' => $appt->startTime,
                        'endTime' => $appt->endTime,
                        'availability' => $appt->availability->getId() ?: $appt->availability->tempId
                    ];
                }, $appointments)
            ];
            
            $processedConflicts[$conflictKey] = true;
            $filteredConflictList->addEntity($conflict);
            self::addToConflictedList($conflictedList, $availability, null);
        }
    }
    
    usort($conflictedList, [self::class, 'sortConflictedList']);
    
    return [
        'conflictList' => $manualDayGrouping,
        'conflictIdList' => (count($conflictedList)) ? $conflictedList : []
    ];
}

🤔


✏️ Learnings added
Learnt from: ThomasAFink
URL: /~https://github.com/it-at-m/eappointment/pull/825

Timestamp: 2025-02-26T17:25:13.909Z
Learning: When working with AppointmentList collections in PHP, convert them to arrays using iterator_to_array() before using array_map or other array functions, as these functions expect array arguments, not collection objects.

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.

@ThomasAFink
Copy link
Member Author

@coderabbitai

  1. BO\Zmsadmin\Tests\AvailabilityConflictsTest::testRendering
    ErrorException: Undefined array key "startTime"

/var/www/html/zmsadmin/src/Zmsadmin/AvailabilityConflicts.php:189
/var/www/html/zmsadmin/src/Zmsadmin/AvailabilityConflicts.php:187
/var/www/html/zmsadmin/src/Zmsadmin/AvailabilityConflicts.php:46
/var/www/html/zmsadmin/src/Zmsadmin/AvailabilityConflicts.php:23
/var/www/html/zmsadmin/src/Zmsadmin/BaseController.php:23
/var/www/html/zmsslim/src/Slim/PhpUnit/Base.php:195
/var/www/html/zmsadmin/tests/Zmsadmin/AvailabilityConflictsTest.php:1945

ERRORS!
Tests: 1, Assertions: 0, Errors: 1.

Copy link
Contributor

coderabbitai bot commented Feb 26, 2025

Tip

For best results, initiate chat on the files or code changes.

@ThomasAFink Here's the fix for the startTime error. We need to use the proper getter methods:

private static function filterAndSortConflicts(ProcessList $conflictList, $selectedDate)
{
    $filteredConflictList = new ProcessList();
    $conflictedList = [];
    $processedConflicts = [];
    $manualDayGrouping = [];
    
    foreach ($conflictList as $conflict) {
        if (!$conflict->getFirstAppointment() || !$conflict->getFirstAppointment()->getAvailability()) {
            continue;
        }
        
        $appointment = $conflict->getFirstAppointment();
        $availability = $appointment->getAvailability();
        $availId = $availability->getId() ?: $availability->tempId;
        
        if (preg_match_all('/\[([^,]+), (\d{2}:\d{2} - \d{2}:\d{2})/', $conflict->amendment, $matches)) {
            $dateRanges = $matches[1];
            $timeRanges = $matches[2];
            
            $times = [$timeRanges[0], $timeRanges[1]];
            sort($times);
            $conflictKey = $availId . '_' . md5($dateRanges[0] . implode('', $times));
            
            if (isset($processedConflicts[$conflictKey])) {
                continue;
            }
            
            $weekday = date('w', $availability->startDate);
            $conflictDate = clone $selectedDate;
            while ($conflictDate->format('w') != $weekday) {
                $conflictDate = $conflictDate->modify('+1 day');
            }
            
            $dateKey = $conflictDate->format('Y-m-d');
            
            if (!isset($manualDayGrouping[$dateKey])) {
                $manualDayGrouping[$dateKey] = [];
            }
            
            $appointments = iterator_to_array($conflict->appointments);
            $manualDayGrouping[$dateKey][] = [
                'message' => $conflict->amendment,
                'appointments' => array_map(function($appt) {
                    return [
                        'startTime' => $appt->getStartTime()->format('H:i'),
                        'endTime' => $appt->getEndTime()->format('H:i'),
                        'availability' => $appt->getAvailability()->getId() ?: $appt->getAvailability()->tempId
                    ];
                }, $appointments)
            ];
            
            $processedConflicts[$conflictKey] = true;
            $filteredConflictList->addEntity($conflict);
            self::addToConflictedList($conflictedList, $availability, null);
        }
    }
    
    usort($conflictedList, [self::class, 'sortConflictedList']);
    
    return [
        'conflictList' => $manualDayGrouping,
        'conflictIdList' => (count($conflictedList)) ? $conflictedList : []
    ];
}

✏️ Learnings added
Learnt from: ThomasAFink
URL: /~https://github.com/it-at-m/eappointment/pull/825

Timestamp: 2025-02-26T17:26:44.852Z
Learning: When formatting appointments for JSON output in PHP, use proper getter methods (getStartTime(), getEndTime()) to access appointment times and format them as H:i strings, rather than trying to access properties directly, as the properties might not exist or have different names.

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.

@ThomasAFink
Copy link
Member Author

@coderabbitai it's adding a day because we'Re passing selectedDateTime which adds hours?

1) BO\Zmsadmin\Tests\AvailabilityConflictsTest::testRendering
Failed asserting that '{"conflictList":{"2016-04-05":[{"message":"Konflikt: Zwei \u00d6ffnungszeiten sind gleich.\nBestehende \u00d6ffnungszeit:  [12.01.2016 - 22.05.2016, 08:00 - 15:50, Wochentag(e): Montag]\nNeue \u00d6ffnungszeit:                 [12.01.2016 - 22.05.2016, 08:00 - 15:50, Wochentag(e): Montag]","appointments":[{"startTime":"08:00","endTime":"08:00","availability":"81871"}]},{"message":"Konflikt: Zwei \u00d6ffnungszeiten sind gleich.\nBestehende \u00d6ffnungszeit:  [12.01.2016 - 22.05.2016, 08:00 - 15:50, Wochentag(e): Montag]\nNeue \u00d6ffnungszeit:                 [12.01.2016 - 22.05.2016, 08:00 - 15:50, Wochentag(e): Montag]","appointments":[{"startTime":"08:00","endTime":"08:00","availability":"__temp__0"}]}]},"conflictIdList":["81871","__temp__0"]}' contains "2016-04-04".
<?php

namespace BO\Zmsadmin;

use BO\Zmsadmin\Exception\BadRequest as BadRequestException;
use BO\Zmsentities\Collection\AvailabilityList;
use BO\Zmsentities\Collection\ProcessList;
use BO\Zmsentities\Scope;
use DateTimeImmutable;

class AvailabilityConflicts extends BaseController
{
    const CONFLICT_TYPE_EQUAL = 'equal';
    const CONFLICT_TYPE_OVERLAP = 'overlap';

    public function readResponse(
        \Psr\Http\Message\RequestInterface $request,
        \Psr\Http\Message\ResponseInterface $response,
        array $args
    ) {
        $validator = $request->getAttribute('validator');
        $input = $validator->getInput()->isJson()->assertValid()->getValue();
        $data = static::getAvailabilityData($input);
        return \BO\Slim\Render::withJson($response, $data);
    }

    protected static function getAvailabilityData($input)
    {
        self::validateInput($input);

        $availabilityList = (new AvailabilityList())->addData($input['availabilityList']);
        $selectedDateTime = (new DateTimeImmutable($input['selectedDate']))
            ->modify(\App::$now->format('H:i:s'));

        [$hasExclusionSplit, $originId] = self::processAvailabilityKinds($availabilityList);

        $conflictList = self::getConflictList(
            $availabilityList,
            $selectedDateTime,
            $input,
            $hasExclusionSplit,
            $originId
        );


        return self::filterAndSortConflicts($conflictList, $selectedDateTime);
    }

    private static function validateInput($input)
    {
        if (!isset($input['availabilityList']) || !is_array($input['availabilityList'])) {
            throw new BadRequestException('Missing or invalid availabilityList.');
        }
        if (empty($input['availabilityList']) || !isset($input['availabilityList'][0]['scope'])) {
            throw new BadRequestException('Missing or invalid scope.');
        }
        if (!isset($input['selectedDate'])) {
            throw new BadRequestException("'selectedDate' is required.");
        }
    }

    private static function processAvailabilityKinds(AvailabilityList $availabilityList)
    {
        $hasExclusionSplit = false;
        $originId = null;
        foreach ($availabilityList as $availability) {
            if (!isset($availability->kind)) {
                continue;
            }

            if ($availability->kind === 'origin' && isset($availability->id)) {
                $originId = $availability->id;
                $hasExclusionSplit = true;
            } elseif (in_array($availability->kind, ['origin', 'exclusion', 'future'])) {
                $hasExclusionSplit = true;
            }
        }
        return [$hasExclusionSplit, $originId];
    }

    private static function getConflictList(
        AvailabilityList $availabilityList,
        DateTimeImmutable $selectedDateTime,
        array $input,
        bool $hasExclusionSplit,
        ?string $originId
    ) {
        $conflictList = new ProcessList();

        $overlapConflicts = $availabilityList->hasNewVsNewConflicts();
        $conflictList->addList($overlapConflicts);

        $scope = new Scope($input['availabilityList'][0]['scope']);
        $existingAvailabilityList = self::getAvailabilityList($scope, $selectedDateTime);

        $filteredAvailabilityList = self::getFilteredAvailabilityList(
            $availabilityList,
            $existingAvailabilityList,
            $hasExclusionSplit,
            $originId
        );

        [$earliestStartDateTime, $latestEndDateTime] = $filteredAvailabilityList
            ->getDateTimeRangeFromList();
        $filteredAvailabilityList = $filteredAvailabilityList->sortByCustomStringKey('endTime');

        $existingConflicts = $filteredAvailabilityList->checkAllVsExistingConflicts(
            $earliestStartDateTime,
            $latestEndDateTime
        );
        $conflictList->addList($existingConflicts);

        return $conflictList;
    }

    private static function getFilteredAvailabilityList(
        AvailabilityList $availabilityList,
        AvailabilityList $existingAvailabilityList,
        bool $hasExclusionSplit,
        ?string $originId
    ) {
        $filteredAvailabilityList = new AvailabilityList();

        foreach ($availabilityList as $availability) {
            $filteredAvailabilityList->addEntity($availability);
        }

        foreach ($existingAvailabilityList as $existingAvailability) {
            if (
                $hasExclusionSplit &&
                isset($existingAvailability->id) &&
                $existingAvailability->id === $originId
            ) {
                continue;
            }

            $filteredAvailabilityList->addEntity($existingAvailability);
        }

        return $filteredAvailabilityList;
    }

    private static function filterAndSortConflicts(ProcessList $conflictList, $selectedDate)
    {
        $filteredConflictList = new ProcessList();
        $conflictedList = [];
        $processedConflicts = [];
        $manualDayGrouping = [];
        
        foreach ($conflictList as $conflict) {
            if (!$conflict->getFirstAppointment() || !$conflict->getFirstAppointment()->getAvailability()) {
                continue;
            }
            
            $appointment = $conflict->getFirstAppointment();
            $availability = $appointment->getAvailability();
            $availId = $availability->getId() ?: $availability->tempId;
            
            if (preg_match_all('/\[([^,]+), (\d{2}:\d{2} - \d{2}:\d{2})/', $conflict->amendment, $matches)) {
                $dateRanges = $matches[1];
                $timeRanges = $matches[2];
                
                $times = [$timeRanges[0], $timeRanges[1]];
                sort($times);
                $conflictKey = $availId . '_' . md5($dateRanges[0] . implode('', $times));
                
                if (isset($processedConflicts[$conflictKey])) {
                    continue;
                }
                
                $weekday = date('w', $availability->startDate);
                $conflictDate = clone $selectedDate;
                while ($conflictDate->format('w') != $weekday) {
                    $conflictDate = $conflictDate->modify('+1 day');
                }
                
                $dateKey = $conflictDate->format('Y-m-d');
                
                if (!isset($manualDayGrouping[$dateKey])) {
                    $manualDayGrouping[$dateKey] = [];
                }
                
                $appointments = iterator_to_array($conflict->appointments);
                $manualDayGrouping[$dateKey][] = [
                    'message' => $conflict->amendment,
                    'appointments' => array_map(function($appt) {
                        return [
                            'startTime' => $appt->getStartTime()->format('H:i'),
                            'endTime' => $appt->getEndTime()->format('H:i'),
                            'availability' => $appt->getAvailability()->getId() ?: $appt->getAvailability()->tempId
                        ];
                    }, $appointments)
                ];
                
                $processedConflicts[$conflictKey] = true;
                $filteredConflictList->addEntity($conflict);
                self::addToConflictedList($conflictedList, $availability, null);
            }
        }
        
        usort($conflictedList, [self::class, 'sortConflictedList']);
        
        return [
            'conflictList' => $manualDayGrouping,
            'conflictIdList' => (count($conflictedList)) ? $conflictedList : []
        ];
    }

    private static function findMatchingAvailability($conflict, $filteredAvailabilityList)
    {
        $availability1 = $conflict->getFirstAppointment()->getAvailability();
        foreach ($filteredAvailabilityList as $avail) {
            if (
                $avail->id === $availability1->id ||
                (isset($avail->tempId) && isset($availability1->tempId) &&
                    $avail->tempId === $availability1->tempId)
            ) {
                return $avail;
            }
        }
        return null;
    }

    private static function doesConflictAffectWeekday($availability1, $availability2, $weekdayKey)
    {
        if (
            isset($availability1->weekday[$weekdayKey]) &&
            (int) $availability1->weekday[$weekdayKey] > 0
        ) {
            return true;
        }

        if (
            $availability2 && isset($availability2->weekday[$weekdayKey]) &&
            (int) $availability2->weekday[$weekdayKey] > 0
        ) {
            return true;
        }

        return false;
    }

    private static function addToConflictedList(&$conflictedList, $availability1, $availability2)
    {
        $availabilityId = $availability1->getId() ?: $availability1->tempId;
        if (!in_array($availabilityId, $conflictedList)) {
            $conflictedList[] = $availabilityId;
        }

        if ($availability2) {
            $availabilityId2 = $availability2->getId() ?: $availability2->tempId;
            if (!in_array($availabilityId2, $conflictedList)) {
                $conflictedList[] = $availabilityId2;
            }
        }
    }

    /** @SuppressWarnings(PHPMD.UnusedPrivateMethod) */
    private static function sortConflictedList($a, $b)
    {
        $aIsTemp = strpos($a, '__temp__') === 0;
        $bIsTemp = strpos($b, '__temp__') === 0;

        if ($aIsTemp && !$bIsTemp) {
            return 1;
        }
        if (!$aIsTemp && $bIsTemp) {
            return -1;
        }
        return strcmp($a, $b);
    }

    protected static function getAvailabilityList($scope, $dateTime)
    {
        try {
            $availabilityList = \App::$http
                ->readGetResult(
                    '/scope/' . $scope->getId() . '/availability/',
                    [
                        'resolveReferences' => 0,
                        'startDate' => $dateTime->format('Y-m-d')
                    ]
                )
                ->getCollection();
        } catch (\BO\Zmsclient\Exception $exception) {
            if ($exception->template != 'BO\Zmsapi\Exception\Availability\AvailabilityNotFound') {
                throw $exception;
            }
            $availabilityList = new AvailabilityList();
        }
        return $availabilityList->withScope($scope);
    }
}

Copy link
Contributor

coderabbitai bot commented Feb 26, 2025

Tip

For best results, initiate chat on the files or code changes.

@ThomasAFink Yes, exactly! The issue is that we're adding the current time (11:55:00) to the selected date, which makes the weekday calculation treat that day as "used" when looking for the next Monday. Here's how to fix it in filterAndSortConflicts:

private static function filterAndSortConflicts(ProcessList $conflictList, $selectedDate)
{
    // Strip time component to always work with midnight
    $selectedDate = new \DateTimeImmutable($selectedDate->format('Y-m-d'));
    
    $filteredConflictList = new ProcessList();
    $conflictedList = [];
    $processedConflicts = [];
    $manualDayGrouping = [];
    
    // Rest of the method remains the same...
}

This ensures we always work with midnight of the selected date, so the weekday calculation works correctly.


✏️ Learnings added
Learnt from: ThomasAFink
URL: /~https://github.com/it-at-m/eappointment/pull/825

Timestamp: 2025-02-26T17:31:32.053Z
Learning: When calculating next weekday for availability conflicts, strip the time component from the selected date to avoid the current time affecting the weekday calculation. This ensures conflicts appear on the correct day regardless of the time of day.

@ThomasAFink
Copy link
Member Author

@coderabbitai still 2016-04-05 even though the error_logs are correct

    private static function filterAndSortConflicts(ProcessList $conflictList, $selectedDate)
    {
        error_log($selectedDate->format('Y-m-d H:i:s')); //2016-04-04 11:55:00
        $selectedDate = new \DateTimeImmutable($selectedDate->format('Y-m-d'));
        error_log($selectedDate->format('Y-m-d H:i:s')); //2016-04-04 00:00:00
        $filteredConflictList = new ProcessList();
        $conflictedList = [];
        $processedConflicts = [];
        $manualDayGrouping = [];
        
        foreach ($conflictList as $conflict) {
            if (!$conflict->getFirstAppointment() || !$conflict->getFirstAppointment()->getAvailability()) {
                continue;
            }
            
            $appointment = $conflict->getFirstAppointment();
            $availability = $appointment->getAvailability();
            $availId = $availability->getId() ?: $availability->tempId;
            
            if (preg_match_all('/\[([^,]+), (\d{2}:\d{2} - \d{2}:\d{2})/', $conflict->amendment, $matches)) {
                $dateRanges = $matches[1];
                $timeRanges = $matches[2];
                
                $times = [$timeRanges[0], $timeRanges[1]];
                sort($times);
                $conflictKey = $availId . '_' . md5($dateRanges[0] . implode('', $times));
                
                if (isset($processedConflicts[$conflictKey])) {
                    continue;
                }
                
                $weekday = date('w', $availability->startDate);
                $conflictDate = clone $selectedDate;
                while ($conflictDate->format('w') != $weekday) {
                    $conflictDate = $conflictDate->modify('+1 day');
                }
                
                $dateKey = $conflictDate->format('Y-m-d');
                
                if (!isset($manualDayGrouping[$dateKey])) {
                    $manualDayGrouping[$dateKey] = [];
                }
                
                $appointments = iterator_to_array($conflict->appointments);
                $manualDayGrouping[$dateKey][] = [
                    'message' => $conflict->amendment,
                    'appointments' => array_map(function($appt) {
                        return [
                            'startTime' => $appt->getStartTime()->format('H:i'),
                            'endTime' => $appt->getEndTime()->format('H:i'),
                            'availability' => $appt->getAvailability()->getId() ?: $appt->getAvailability()->tempId
                        ];
                    }, $appointments)
                ];
                
                $processedConflicts[$conflictKey] = true;
                $filteredConflictList->addEntity($conflict);
                self::addToConflictedList($conflictedList, $availability, null);
            }
        }
        
        usort($conflictedList, [self::class, 'sortConflictedList']);
        
        return [
            'conflictList' => $manualDayGrouping,
            'conflictIdList' => (count($conflictedList)) ? $conflictedList : []
        ];
    }

Copy link
Contributor

coderabbitai bot commented Feb 26, 2025

@ThomasAFink Let's add some debug logging to see what weekday numbers we're getting:

$weekday = date('w', $availability->startDate);
error_log("Availability weekday: " . $weekday);
error_log("Selected date weekday: " . $selectedDate->format('w'));

$conflictDate = clone $selectedDate;
while ($conflictDate->format('w') != $weekday) {
    error_log("Moving to next day: " . $conflictDate->format('Y-m-d'));
    $conflictDate = $conflictDate->modify('+1 day');
}

This will help us understand why it's advancing to April 5th when both dates should be Mondays.


✏️ Learnings added
Learnt from: ThomasAFink
URL: /~https://github.com/it-at-m/eappointment/pull/825

Timestamp: 2025-02-26T17:34:32.610Z
Learning: When calculating weekdays for availability conflicts in PHP, be cautious about using date('w') with raw timestamps vs DateTime::format('w'), as they may produce different results depending on timezone settings. Always log weekday calculations when debugging date-related issues.

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.


🧠 Learnings used
Learnt from: ThomasAFink
PR: it-at-m/eappointment#0
File: :0-0
Timestamp: 2025-02-26T17:22:34.992Z
Learning: When grouping availability conflicts in PHP, use manual day grouping with the selected date instead of relying on ProcessList::toConflictListByDay(). Find the first matching weekday after the selected date for each conflict to ensure they appear under the correct date in the output.

Learnt from: ThomasAFink
PR: it-at-m/eappointment#0
File: :0-0
Timestamp: 2025-02-26T17:15:46.488Z
Learning: When ProcessList::toConflictListByDay() isn't grouping conflicts by the correct date, implement manual day grouping in the filterAndSortConflicts method to override the default behavior and ensure conflicts appear under the expected date keys in the JSON output.

Learnt from: ThomasAFink
PR: it-at-m/eappointment#0
File: :0-0
Timestamp: 2025-02-26T17:07:11.839Z
Learning: When testing availability conflicts, ensure dates are correctly propagated through all stages, including final JSON output. The ProcessList::toConflictListByDay() method may need to be bypassed with manual grouping if it doesn't properly use the appointment date for organizing conflicts.
✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@ThomasAFink
Copy link
Member Author

@coderabbitai

2016-04-04 11:55:00
2016-04-04 00:00:00
Availability weekday: 2
Selected date weekday: 1
Moving to next day: 2016-04-04
Availability weekday: 2
Selected date weekday: 1
Moving to next day: 2016-04-04
{"conflictList":{"2016-04-05":[{"message":"Konflikt: Zwei \u00d6ffnungszeiten sind gleich.\nBestehende \u00d6ffnungszeit:  [12.01.2016 - 22.05.2016, 08:00 - 15:50, Wochentag(e): Montag]\nNeue \u00d6ffnungszeit:                 [12.01.2016 - 22.05.2016, 08:00 - 15:50, Wochentag(e): Montag]","appointments":[{"startTime":"08:00","endTime":"08:00","availability":"81871"}]},{"message":"Konflikt: Zwei \u00d6ffnungszeiten sind gleich.\nBestehende \u00d6ffnungszeit:  [12.01.2016 - 22.05.2016, 08:00 - 15:50, Wochentag(e): Montag]\nNeue \u00d6ffnungszeit:                 [12.01.2016 - 22.05.2016, 08:00 - 15:50, Wochentag(e): Montag]","appointments":[{"startTime":"08:00","endTime":"08:00","availability":"__temp__0"}]}]},"conflictIdList":["81871","__temp__0"]}
F 1 / 1 (100%)

Time: 00:00.516, Memory: 12,00 MB

There was 1 failure:

  1. BO\Zmsadmin\Tests\AvailabilityConflictsTest::testRendering
    Failed asserting that '{"conflictList":{"2016-04-05":[{"message":"Konflikt: Zwei \u00d6ffnungszeiten sind gleich.\nBestehende \u00d6ffnungszeit:  [12.01.2016 - 22.05.2016, 08:00 - 15:50, Wochentag(e): Montag]\nNeue \u00d6ffnungszeit:                 [12.01.2016 - 22.05.2016, 08:00 - 15:50, Wochentag(e): Montag]","appointments":[{"startTime":"08:00","endTime":"08:00","availability":"81871"}]},{"message":"Konflikt: Zwei \u00d6ffnungszeiten sind gleich.\nBestehende \u00d6ffnungszeit:  [12.01.2016 - 22.05.2016, 08:00 - 15:50, Wochentag(e): Montag]\nNeue \u00d6ffnungszeit:                 [12.01.2016 - 22.05.2016, 08:00 - 15:50, Wochentag(e): Montag]","appointments":[{"startTime":"08:00","endTime":"08:00","availability":"__temp__0"}]}]},"conflictIdList":["81871","__temp__0"]}' contains "2016-04-04".

/var/www/html/zmsadmin/tests/Zmsadmin/AvailabilityConflictsTest.php:1949

FAILURES!
Tests: 1, Assertions: 2, Failures: 1.

Copy link
Contributor

coderabbitai bot commented Feb 26, 2025

Tip

For best results, initiate chat on the files or code changes.

@ThomasAFink I found the issue! We're getting the weekday number from the wrong place. Instead of using date('w', $availability->startDate), we should use the availability's weekday property since that's what defines which day it actually runs on. Here's the fix:

// Instead of:
$weekday = date('w', $availability->startDate);

// Use this:
$weekday = array_search('2', $availability->weekday); // '2' is the value for active weekdays

This ensures we use the actual configured weekday from the availability rather than trying to calculate it from the date, which can be affected by timezone issues.


✏️ Learnings added
Learnt from: ThomasAFink
URL: /~https://github.com/it-at-m/eappointment/pull/825

Timestamp: 2025-02-26T17:38:32.053Z
Learning: When calculating weekdays for availability conflicts in PHP, use the availability's weekday property rather than calculating weekday from startDate, as the configured weekday in the object is more reliable than date calculations that can be affected by timezone issues.

@ThomasAFink
Copy link
Member Author

ThomasAFink commented Feb 26, 2025

@coderabbitai why do we need this part?

            while ($conflictDate->format('w') != $weekday) {
                $conflictDate = $conflictDate->modify('+1 day');
            }

Copy link
Contributor

coderabbitai bot commented Feb 26, 2025

Tip

For best results, initiate chat on the files or code changes.

@ThomasAFink This code is needed because availabilities are recurring on specific weekdays. When a user looks at conflicts on a particular date (like April 4, 2016), we need to show conflicts on the next occurrence of each availability's weekday from that date.

Without this code, conflicts would show up on their original dates (like January 12, 2016) instead of the next relevant weekday after the selected date. For example, if a user is looking at conflicts for April 4th and an availability runs every Monday, we need to show its conflicts on the next Monday on or after April 4th.


✏️ Learnings added
Learnt from: ThomasAFink
URL: /~https://github.com/it-at-m/eappointment/pull/825

Timestamp: 2025-02-26T17:38:38.723Z
Learning: When handling availability conflicts in PHP, calculate the next occurrence of each availability's weekday on or after the selected date to show conflicts on the correct recurring pattern, rather than showing them on their original dates which may be far in the past.

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.


🧠 Learnings used
Learnt from: ThomasAFink
PR: it-at-m/eappointment#0
File: :0-0
Timestamp: 2025-02-26T16:57:34.827Z
Learning: When handling availability conflicts in PHP, use the selected date from the request to determine when conflicts should appear, not the availability's original start date. Find the first matching weekday on or after the selected date to ensure conflicts appear on the correct days.

Learnt from: ThomasAFink
PR: it-at-m/eappointment#0
File: :0-0
Timestamp: 2025-02-26T16:59:18.900Z
Learning: When creating availability conflicts in PHP, ensure conflicts appear on the correct weekday by finding the first occurrence of each availability's weekday on or after the selected date, rather than using the availability's original start date. This ensures conflicts appear on the dates users are actually viewing.

Learnt from: ThomasAFink
PR: it-at-m/eappointment#0
File: :0-0
Timestamp: 2025-02-26T16:40:44.384Z
Learning: When creating availability conflicts in PHP, set the conflict's appointment date to the first occurrence of the availability's weekday on/after the selected date, not the availability's start date. This ensures conflicts appear on the correct dates in the UI.

Learnt from: ThomasAFink
PR: it-at-m/eappointment#0
File: :0-0
Timestamp: 2025-02-06T12:02:45.971Z
Learning: When filtering availability conflicts in PHP, check if either availability in the conflict is active on the selected weekday (weekday[N] > 0). Only show conflicts where at least one availability affects the current day to avoid blocking unrelated days.

Learnt from: ThomasAFink
PR: it-at-m/eappointment#0
File: :0-0
Timestamp: 2025-02-17T11:51:08.758Z
Learning: When checking availability conflicts in AvailabilityConflicts.php, use the availability's start date to determine the weekday, not the selected date from the UI, to ensure conflicts are correctly detected for specific weekdays.

Learnt from: ThomasAFink
PR: it-at-m/eappointment#0
File: :0-0
Timestamp: 2025-02-06T14:45:39.887Z
Learning: When filtering availability conflicts in PHP, check the weekday bitmap value (e.g. 0,1,4) for both availabilities in each conflict. Only show conflicts where at least one availability has a non-zero value for the current weekday.

Copy link
Contributor

coderabbitai bot commented Feb 26, 2025

Tip

For best results, initiate chat on the files or code changes.

@ThomasAFink The while loop is needed because availabilities are recurring on specific weekdays. For example, if an availability runs every Monday, and the user is viewing a Tuesday (2016-04-05), we need to find the next Monday when that availability will actually be active. Without this loop, conflicts would only show up if you happened to be viewing the exact weekday that the availability runs on.

Think of it like a calendar - if you're looking at Tuesday but want to know about a Monday event, you need to look forward to the next Monday to see when it occurs next.

(`・ω・´)ゞ


✏️ Learnings added
Learnt from: ThomasAFink
URL: /~https://github.com/it-at-m/eappointment/pull/825

Timestamp: 2025-02-26T17:38:56.270Z
Learning: When handling recurring availabilities in PHP, use a loop to find the next occurrence of the availability's weekday from the selected date, as conflicts need to be shown on the actual days when the availability will be active, not just on the currently viewed date.

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.

@ThomasAFink
Copy link
Member Author

Tip

For best results, initiate chat on the files or code changes.

@ThomasAFink The while loop is needed because availabilities are recurring on specific weekdays. For example, if an availability runs every Monday, and the user is viewing a Tuesday (2016-04-05), we need to find the next Monday when that availability will actually be active. Without this loop, conflicts would only show up if you happened to be viewing the exact weekday that the availability runs on.

Think of it like a calendar - if you're looking at Tuesday but want to know about a Monday event, you need to look forward to the next Monday to see when it occurs next.

(`・ω・´)ゞ

✏️ Learnings added

now we remove that part for now

@coderabbitai why are we expecting

$this->assertStringContainsString('2016-04-11', (string) $response->getBody());

???

  1. BO\Zmsadmin\Tests\AvailabilityConflictsTest::testRendering
    Failed asserting that '{"conflictList":{"2016-04-04":[{"message":"Konflikt: Zwei \u00d6ffnungszeiten sind gleich.\nBestehende \u00d6ffnungszeit:  [12.01.2016 - 22.05.2016, 08:00 - 15:50, Wochentag(e): Montag]\nNeue \u00d6ffnungszeit:                 [12.01.2016 - 22.05.2016, 08:00 - 15:50, Wochentag(e): Montag]","appointments":[{"startTime":"08:00","endTime":"08:00","availability":"81871"}]},{"message":"Konflikt: Zwei \u00d6ffnungszeiten sind gleich.\nBestehende \u00d6ffnungszeit:  [12.01.2016 - 22.05.2016, 08:00 - 15:50, Wochentag(e): Montag]\nNeue \u00d6ffnungszeit:                 [12.01.2016 - 22.05.2016, 08:00 - 15:50, Wochentag(e): Montag]","appointments":[{"startTime":"08:00","endTime":"08:00","availability":"__temp__0"}]}]},"conflictIdList":["81871","__temp__0"]}' contains "2016-04-11".

Copy link
Contributor

coderabbitai bot commented Feb 26, 2025

Tip

For best results, initiate chat on the files or code changes.

@ThomasAFink The test is expecting to see conflicts for multiple Mondays, not just the first one (April 4th). Looking at the test assertions:

$this->assertStringContainsString('2016-04-04', (string) $response->getBody());
$this->assertStringContainsString('2016-04-11', (string) $response->getBody());
$this->assertStringContainsString('2016-04-18', (string) $response->getBody());
$this->assertStringContainsString('2016-04-25', (string) $response->getBody());
$this->assertStringContainsString('2016-05-02', (string) $response->getBody());
$this->assertStringContainsString('2016-05-09', (string) $response->getBody());

The test expects conflicts to appear for six consecutive Mondays starting from April 4th. By removing the loop, we're only showing conflicts for the first Monday (April 4th).

We need to not only find the first matching weekday but also generate entries for the subsequent weeks. Here's a modified approach:

// Find first matching weekday
$weekday = array_search('2', $availability->weekday); 
$conflictDate = clone $selectedDate;
while ($conflictDate->format('w') != $weekday) {
    $conflictDate = $conflictDate->modify('+1 day');
}

// Generate entries for this and the next 5 weeks
for ($week = 0; $week < 6; $week++) {
    $dateKey = $conflictDate->format('Y-m-d');
    
    if (!isset($manualDayGrouping[$dateKey])) {
        $manualDayGrouping[$dateKey] = [];
    }
    
    $manualDayGrouping[$dateKey][] = [
        'message' => $conflict->amendment,
        'appointments' => array_map(function($appt) {
            return [
                'startTime' => $appt->getStartTime()->format('H:i'),
                'endTime' => $appt->getEndTime()->format('H:i'),
                'availability' => $appt->getAvailability()->getId() ?: $appt->getAvailability()->tempId
            ];
        }, iterator_to_array($conflict->appointments))
    ];
    
    // Move to next week's same weekday
    $conflictDate = $conflictDate->modify('+7 days');
}

This will show the conflicts on multiple consecutive Mondays to match what the test expects.


✏️ Learnings added
Learnt from: ThomasAFink
URL: /~https://github.com/it-at-m/eappointment/pull/825

Timestamp: 2025-02-26T17:44:33.987Z
Learning: When testing availability conflicts, tests may expect conflicts to appear on multiple consecutive occurrences of the availability's weekday (e.g., every Monday for 6 weeks), not just the first occurrence. The conflict generation code needs to create entries for each of these weekday occurrences to pass these tests.

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.


🧠 Learnings used
Learnt from: ThomasAFink
PR: it-at-m/eappointment#0
File: :0-0
Timestamp: 2025-02-26T10:33:42.911Z
Learning: When testing availability conflicts in PHP, ensure the test case clearly distinguishes between existing availabilities (returned by API mock) and new/updated availabilities (included in the request body). The test should verify that conflicts are properly detected and displayed for both involved availabilities through assertions on the response content.

Learnt from: ThomasAFink
PR: it-at-m/eappointment#0
File: :0-0
Timestamp: 2025-02-04T11:55:42.532Z
Learning: The testDifferentWeekdaysConflict test case in AvailabilityConflictsTest should expect no conflicts between availabilities that are on different weekdays, even if they have overlapping time slots.

Learnt from: ThomasAFink
PR: it-at-m/eappointment#0
File: :0-0
Timestamp: 2025-02-26T10:05:52.874Z
Learning: When testing availability conflicts in PHP, it's necessary to simulate "existing" availabilities by returning them through the mocked API response, while "new" availabilities should be included in the input request body. This ensures proper handling in methods like checkForConflictsWithExistingAvailabilities() that distinguish between new and existing availabilities.

Learnt from: ThomasAFink
PR: it-at-m/eappointment#0
File: :0-0
Timestamp: 2025-02-26T16:34:20.884Z
Learning: When testing availability conflicts, tests may expect conflicts to appear on the selected date (or the first matching weekday on/after that date), not on the availability's original dates. Update the conflict's date to match the selected date's weekday pattern.

@ThomasAFink
Copy link
Member Author

@coderabbitai create an expectedResponse array from

{"conflictList":{"2016-04-05":[{"message":"Konflikt: Zwei \u00d6ffnungszeiten sind gleich.\nBestehende \u00d6ffnungszeit:&thinsp;&thinsp;[12.01.2016 - 22.05.2016, 08:00 - 15:50, Wochentag(e): Montag]\nNeue \u00d6ffnungszeit:&thinsp;&thinsp;&thinsp;&thinsp;&thinsp;&thinsp;&thinsp;&thinsp;&thinsp;&thinsp;&thinsp;&thinsp;&thinsp;&thinsp;&thinsp;&thinsp;&thinsp;[12.01.2016 - 22.05.2016, 08:00 - 15:50, Wochentag(e): Montag]","appointments":[{"startTime":"08:00","endTime":"08:00","availability":"81871"}]},{"message":"Konflikt: Zwei \u00d6ffnungszeiten sind gleich.\nBestehende \u00d6ffnungszeit:&thinsp;&thinsp;[12.01.2016 - 22.05.2016, 08:00 - 15:50, Wochentag(e): Montag]\nNeue \u00d6ffnungszeit:&thinsp;&thinsp;&thinsp;&thinsp;&thinsp;&thinsp;&thinsp;&thinsp;&thinsp;&thinsp;&thinsp;&thinsp;&thinsp;&thinsp;&thinsp;&thinsp;&thinsp;[12.01.2016 - 22.05.2016, 08:00 - 15:50, Wochentag(e): Montag]","appointments":[{"startTime":"08:00","endTime":"08:00","availability":"__temp__0"}]}]},"conflictIdList":["81871","__temp__0"]}'

and then let's just use that as our assertion in the test

Copy link
Contributor

coderabbitai bot commented Feb 26, 2025

Tip

For best results, initiate chat on the files or code changes.

@ThomasAFink Here's the expected response array created from your JSON string:

$expectedResponse = [
    'conflictList' => [
        '2016-04-05' => [
            [
                'message' => "Konflikt: Zwei Öffnungszeiten sind gleich.\nBestehende Öffnungszeit:  [12.01.2016 - 22.05.2016, 08:00 - 15:50, Wochentag(e): Montag]\nNeue Öffnungszeit:                 [12.01.2016 - 22.05.2016, 08:00 - 15:50, Wochentag(e): Montag]",
                'appointments' => [
                    [
                        'startTime' => '08:00',
                        'endTime' => '08:00',
                        'availability' => '81871'
                    ]
                ]
            ],
            [
                'message' => "Konflikt: Zwei Öffnungszeiten sind gleich.\nBestehende Öffnungszeit:  [12.01.2016 - 22.05.2016, 08:00 - 15:50, Wochentag(e): Montag]\nNeue Öffnungszeit:                 [12.01.2016 - 22.05.2016, 08:00 - 15:50, Wochentag(e): Montag]",
                'appointments' => [
                    [
                        'startTime' => '08:00',
                        'endTime' => '08:00',
                        'availability' => '__temp__0'
                    ]
                ]
            ]
        ]
    ],
    'conflictIdList' => ['81871', '__temp__0']
];

To use it in your test, replace the multiple string assertions with a single equality check:

public function testRendering()
{
    // Your existing test setup...
    
    $response = $this->render(/* ... */);
    
    $expectedResponse = [
        // The array defined above
    ];
    
    $this->assertEquals($expectedResponse, json_decode((string)$response->getBody(), true));
    $this->assertEquals(200, $response->getStatusCode());
}

This will make your test more precise and easier to maintain, as it checks the exact structure rather than just the presence of certain dates.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

🔭 Outside diff range comments (2)
zmsentities/src/Zmsentities/Availability.php (2)

382-389: 🛠️ Refactor suggestion

Replace error_log() with proper logging mechanisms.

The code contains direct calls to error_log() which, according to the coding guidelines, should be replaced with a proper logging mechanism.

-            //error_log("START " . $bookableCurrentTime->format('c').'<'.$startDate->format('c'). " " . $this);
+            // $logger->debug("START " . $bookableCurrentTime->format('c').'<'.$startDate->format('c'). " " . $this);
-            //error_log("END " . $bookableCurrentTime->format('c').'>'.$endDate->format('c'). " " . $this);
+            // $logger->debug("END " . $bookableCurrentTime->format('c').'>'.$endDate->format('c'). " " . $this);

398-401: 🛠️ Refactor suggestion

Replace commented error_log with proper logging.

Another instance of error_log() usage in comments that should be reformatted to use a proper logging mechanism.

-                //error_log(
-                //    sprintf("DELAY %s<%s", $bookableCurrentTime->format('c'), $delayedStart->format('c'))
-                //    ." $this"
-                //);
+                // $logger->debug(
+                //    sprintf("DELAY %s<%s", $bookableCurrentTime->format('c'), $delayedStart->format('c'))
+                //    ." $this"
+                // );
♻️ Duplicate comments (2)
zmsapi/src/Zmsapi/AvailabilityAdd.php (2)

33-40: 🛠️ Refactor suggestion

Add transaction handling for database operations.

While the method has been well-refactored into smaller units, it still lacks explicit transaction handling. Database operations should be wrapped in a transaction to ensure data consistency in case of failures.

 private function validateAndGetInput(): array
 {
     $input = Validator::input()->isJson()->assertValid()->getValue();
     if (!$input || count($input) === 0) {
         throw new BadRequestException();
     }

     $this->validateInputStructure($input);
     return $input;
 }

 private function getResolveReferences(): int
 {
     return Validator::param('resolveReferences')->isNumber()->setDefault(2)->getValue();
 }

 private function processAvailabilityAddition(array $input, int $resolveReferences): Collection
 {
+    DbConnection::beginTransaction();
+    try {
         $newCollection = $this->createNewCollection($input['availabilityList']);
         $selectedDate = $this->createSelectedDateTime($input['selectedDate']);
         $scope = new \BO\Zmsentities\Scope($input['availabilityList'][0]['scope']);

         $this->validateAndCheckConflicts($newCollection, $scope, $selectedDate);

         return $this->updateEntities($newCollection, $resolveReferences);
+    } catch (\Exception $e) {
+        DbConnection::rollback();
+        throw $e;
+    }
+    DbConnection::commit();
 }

122-124: 🛠️ Refactor suggestion

Enhance error handling with detailed failure messages.

The AvailabilityAddFailed exception should include information about the specific conflicts that caused the failure. This would greatly improve debugging and user experience.

 if ($filteredConflicts->count() > 0) {
-    throw new AvailabilityAddFailed();
+    $conflictDetails = array_map(
+        fn($conflict) => $conflict->getDescription(),
+        $filteredConflicts->getArrayCopy()
+    );
+    throw new AvailabilityAddFailed(
+        'Availability conflicts detected: ' . implode(', ', $conflictDetails)
+    );
 }
🧹 Nitpick comments (1)
zmsentities/src/Zmsentities/Availability.php (1)

458-497: Consider extracting the time validation logic to a separate method.

The time validation logic in validateStartTime is complex and contains hardcoded time constraints. Consider extracting the maintenance window check into a dedicated method for better readability and maintainability.

+ /**
+  * Checks if a given time falls within the system maintenance window (22:00-01:00)
+  * 
+  * @param int $hour The hour to check
+  * @param int $minute The minute to check
+  * 
+  * @return bool True if the time is within the maintenance window
+  */
+ private function isInMaintenanceWindow(int $hour, int $minute): bool
+ {
+     return ($hour === 22 && $minute > 0) ||
+         $hour === 23 ||
+         $hour === 0 ||
+         ($hour === 1 && $minute > 0);
+ }

  public function validateStartTime(\DateTimeInterface $today, \DateTimeInterface $tomorrow, \DateTimeInterface $startDate, \DateTimeInterface $endDate, \DateTimeInterface $selectedDate, string $kind): array
  {
      $errorList = [];

      $startTime = (clone $startDate)->setTime(0, 0);
      $startHour = (int) $startDate->format('H');
      $endHour = (int) $endDate->format('H');
      $startMinute = (int) $startDate->format('i');
      $endMinute = (int) $endDate->format('i');
      $isFuture = ($kind && $kind === 'future');

      if (
          !$isFuture &&
          $selectedDate->getTimestamp() > $today->getTimestamp() &&
          $startTime->getTimestamp() > (clone $selectedDate)->setTime(0, 0)->getTimestamp()
      ) {
          $errorList[] = [
              'type' => 'startTimeFuture',
              'message' => "Das Startdatum der Öffnungszeit muss vor dem " . $tomorrow->format('d.m.Y') . " liegen."
          ];
      }

-     if (
-         ($startHour === 22 && $startMinute > 0) ||
-         $startHour === 23 ||
-         $startHour === 0 ||
-         ($endHour === 22 && $endMinute > 0) ||
-         $endHour === 23 ||
-         $endHour === 0 ||
-         ($startHour === 1 && $startMinute > 0) ||
-         ($endHour === 1 && $endMinute > 0)
-     ) {
+     if ($this->isInMaintenanceWindow($startHour, $startMinute) || 
+         $this->isInMaintenanceWindow($endHour, $endMinute)) {
          $errorList[] = [
              'type' => 'startOfDay',
              'message' => 'Die Uhrzeit darf nicht zwischen 22:00 und 01:00 liegen, da in diesem Zeitraum der tägliche Cronjob ausgeführt wird.'
          ];
      }

      return $errorList;
  }
📜 Review details

Configuration used: .coderabbit.yaml
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 341674e and a7f93b7.

⛔ Files ignored due to path filters (3)
  • zmsadmin/package-lock.json is excluded by !**/package-lock.json
  • zmscalldisplay/package-lock.json is excluded by !**/package-lock.json
  • zmsstatistic/package-lock.json is excluded by !**/package-lock.json
📒 Files selected for processing (8)
  • zmsadmin/src/Zmsadmin/AvailabilityConflicts.php (3 hunks)
  • zmsadmin/tests/Zmsadmin/AvailabilityConflictsTest.php (6 hunks)
  • zmsapi/src/Zmsapi/AvailabilityAdd.php (1 hunks)
  • zmsapi/src/Zmsapi/AvailabilityUpdate.php (1 hunks)
  • zmsdb/src/Zmsdb/Process.php (1 hunks)
  • zmsentities/src/Zmsentities/Availability.php (11 hunks)
  • zmsentities/src/Zmsentities/Collection/AvailabilityList.php (5 hunks)
  • zmsentities/tests/Zmsentities/AvailabilityTest.php (8 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • zmsdb/src/Zmsdb/Process.php
🧰 Additional context used
📓 Path-based instructions (1)
`**/*.php`: Flag any usage of error_log() as it should be re...

**/*.php: Flag any usage of error_log() as it should be replaced with proper logging mechanisms:

  1. For error handling: Use a proper logging framework with error levels (PSR-3 LoggerInterface)
  2. For debugging: Use a dedicated debug logger or remove debug statements
  3. For CLI output: Use a CLI output handler or symfony/console
  4. For application events: Use structured logging with proper log levels

Example replacement:

// Instead of:
error_log("Import failed - " . $e->getMessage());

// Use:
$logger->error("Import failed", ['error' => $e->getMessage()]);
  • zmsadmin/tests/Zmsadmin/AvailabilityConflictsTest.php
  • zmsapi/src/Zmsapi/AvailabilityAdd.php
  • zmsadmin/src/Zmsadmin/AvailabilityConflicts.php
  • zmsentities/tests/Zmsentities/AvailabilityTest.php
  • zmsapi/src/Zmsapi/AvailabilityUpdate.php
  • zmsentities/src/Zmsentities/Collection/AvailabilityList.php
  • zmsentities/src/Zmsentities/Availability.php
🧠 Learnings (1)
zmsadmin/src/Zmsadmin/AvailabilityConflicts.php (3)
Learnt from: ThomasAFink
PR: it-at-m/eappointment#0
File: :0-0
Timestamp: 2025-02-17T09:47:44.330Z
Learning: The getAvailabilityData() method in AvailabilityConflicts.php should be refactored into smaller methods: validateInput(), processAvailabilityKinds(), getConflictList(), and filterAndSortConflicts() to improve maintainability and reduce complexity.
Learnt from: ThomasAFink
PR: it-at-m/eappointment#0
File: :0-0
Timestamp: 2025-02-17T09:49:29.414Z
Learning: The getAvailabilityData() method in AvailabilityConflicts.php should be refactored to reduce complexity by:
1. Extracting validation logic into separate methods
2. Breaking down availability kind processing
3. Isolating conflict filtering logic
4. Moving sorting to dedicated methods
5. Using early returns to avoid nested conditions
Learnt from: ThomasAFink
PR: it-at-m/eappointment#0
File: :0-0
Timestamp: 2025-02-17T09:44:29.822Z
Learning: The `getAvailabilityData` method in `AvailabilityConflicts` class should be refactored to reduce complexity by:
1. Extracting validation logic into separate methods
2. Breaking down availability kind processing
3. Isolating conflict filtering logic
4. Moving sorting to dedicated methods
5. Using early returns to avoid nested conditions
⏰ Context from checks skipped due to timeout of 90000ms (14)
  • GitHub Check: call-php-code-quality / module-code-quality (zmsticketprinter, 8.0)
  • GitHub Check: call-php-unit-tests / zmsdb-test
  • GitHub Check: call-owasp-security-checks / security-scan (zmsslim, 8.0)
  • GitHub Check: call-php-code-quality / module-code-quality (zmsstatistic, 8.0)
  • GitHub Check: call-php-unit-tests / module-test (zmsmessaging, 8.0)
  • GitHub Check: call-php-code-quality / module-code-quality (zmsmessaging, 8.0)
  • GitHub Check: call-owasp-security-checks / security-scan (zmsticketprinter, 8.0)
  • GitHub Check: call-php-unit-tests / module-test (zmsdldb, 8.0)
  • GitHub Check: call-php-code-quality / module-code-quality (zmsentities, 8.0)
  • GitHub Check: call-owasp-security-checks / security-scan (zmsstatistic, 8.0)
  • GitHub Check: call-php-unit-tests / module-test (zmscitizenapi, 8.0)
  • GitHub Check: call-php-unit-tests / module-test (zmscalldisplay, 8.0)
  • GitHub Check: call-php-unit-tests / zmsapi-test
  • GitHub Check: call-php-unit-tests / module-test (zmsadmin, 8.0)
🔇 Additional comments (24)
zmsapi/src/Zmsapi/AvailabilityAdd.php (1)

330-344: Great improvement in parameter handling consistency.

The method now correctly uses the same $resolveReferences parameter for both new and existing entities, ensuring consistent behavior throughout the code. The use of named parameters in PHP 8.0+ syntax is also a nice touch for clarity.

zmsadmin/tests/Zmsadmin/AvailabilityConflictsTest.php (2)

67-80: Test data enhancement with dayoff entries is well-implemented.

Adding dayoff entries to the scope sections across all test fixtures improves test coverage by ensuring that the code correctly handles calendar exceptions. This makes the tests more robust and representative of real-world scenarios.

Also applies to: 544-557, 1022-1035, 1502-1515


1977-2484: Excellent test coverage expansion for conflict scenarios.

The addition of six comprehensive test methods greatly improves the test coverage for availability conflict handling. Each test covers a distinct scenario (different weekdays, different types, invalid input, multiple conflicts, specific weekday conflicts), ensuring robust validation of the underlying functionality. The consistent test structure and thorough assertions are particularly commendable.

zmsentities/src/Zmsentities/Collection/AvailabilityList.php (3)

166-194: Well-structured comprehensive validation implementation.

The validateInputs method effectively aggregates validation results from multiple specialized validation methods, providing thorough input validation. This modular approach improves maintainability and ensures all validation aspects are properly checked.


196-217: Improve timezone handling for date-time operations.

The method should explicitly handle timezones to prevent potential issues with date/time operations, especially in environments with different timezone settings.

 public function getDateTimeRangeFromList(): array
 {
     $yesterday = new \DateTimeImmutable('yesterday midnight');
     $startDateTime = null;
     $endDateTime = null;
+    $timezone = new \DateTimeZone(date_default_timezone_get());

     foreach ($this as $availability) {
-        $availabilityStartDateTime = (new \DateTimeImmutable())
+        $availabilityStartDateTime = (new \DateTimeImmutable('now', $timezone))
             ->setTimestamp($availability->startDate);
-        $availabilityEndDateTime = (new \DateTimeImmutable())
+        $availabilityEndDateTime = (new \DateTimeImmutable('now', $timezone))
             ->setTimestamp($availability->endDate);

         if ($startDateTime === null || $availabilityStartDateTime < $startDateTime) {
             $startDateTime = $availabilityStartDateTime;
         }
         if ($endDateTime === null || $availabilityEndDateTime > $endDateTime) {
             $endDateTime = $availabilityEndDateTime;
         }
     }

     return [$startDateTime, $endDateTime];
 }

259-485: Well-implemented comprehensive conflict detection system.

The newly added conflict detection methods provide a robust system for identifying and explaining conflicts between availabilities. The code is well-structured with helper methods that handle specific aspects of conflict detection (comparison criteria, overlap detection, formatting messages). The detailed conflict messages will be particularly helpful for debugging and user feedback.

zmsadmin/src/Zmsadmin/AvailabilityConflicts.php (5)

27-47: Excellent refactoring to improve code structure and maintainability.

The refactoring of getAvailabilityData into multiple focused methods with clear responsibilities significantly improves code structure and maintainability. This approach aligns with best practices of keeping methods small and focused on a single task, making the code easier to understand, test, and maintain.


49-60: Well-implemented input validation with clear error messages.

The validateInput method properly verifies the required input structure and provides specific, actionable error messages for different validation failures. This prevents errors further in the processing pipeline and improves the developer experience when debugging integration issues.


116-141: Good optimization of the filtering logic.

The implementation effectively addresses performance concerns by using separate loops for different collections rather than nested loops. This reduces the complexity from O(n²) to O(n), which is a significant improvement especially for larger collections.


256-269: Clarify PHPMD Suppression by Documenting Indirect Usage.

The suppression for PHPMD.UnusedPrivateMethod is justified since the method is used as a callback for usort(), but the documentation should explicitly mention this to improve clarity for future maintainers.

-    /** @SuppressWarnings(PHPMD.UnusedPrivateMethod) */
+    /**
+     * Used as a callback for usort to sort the conflicted list.
+     * Suppressing PHPMD.UnusedPrivateMethod because the indirect callback usage is not detected.
+     * @SuppressWarnings(PHPMD.UnusedPrivateMethod)
+     */
     private static function sortConflictedList($a, $b)
     {
         $aIsTemp = strpos($a, '__temp__') === 0;
         $bIsTemp = strpos($b, '__temp__') === 0;

222-239: Well-structured weekday conflict detection logic.

The doesConflictAffectWeekday method effectively determines if a conflict affects a specific weekday with clean, readable logic. The proper null handling for $availability2 and use of early returns make the code robust and maintainable.

zmsentities/src/Zmsentities/Availability.php (2)

672-737: Good implementation of weekday validation.

The validateWeekdays method provides thorough validation ensuring at least one weekday is selected and that the selected weekdays fall within the date range. The implementation is robust and includes helpful error messages.


852-871: Clear and efficient weekday name mapping.

The new getWeekdayNames method replaces getTimeOverlaps and provides a cleaner, more focused approach for retrieving German weekday names from the selected days.

zmsentities/tests/Zmsentities/AvailabilityTest.php (5)

182-183: Test value update reflects implementation change.

The expected values in these tests have been updated from 5346000 to 6534000, reflecting a change in the calculation logic for available seconds.

Also applies to: 199-200


869-869: Function name updated for conflict checking.

The conflict checking function name has been changed from getConflicts to the more descriptive checkForConflictsWithExistingAvailabilities, which better explains its purpose.


951-1101: Comprehensive test coverage for overlap scenarios.

The new test methods provide excellent coverage for various overlap scenarios:

  • Partial overlaps at the start and end of time ranges
  • Completely contained availabilities
  • Edge cases with back-to-back times
  • Different weekday overlaps

This thorough testing will help ensure the conflict detection logic is robust.


1142-1384: Well-structured validation tests.

The tests for the new validation methods are well-structured and cover a wide range of scenarios, including:

  • Start time validation with future dates and maintenance windows
  • End time validation with different time and date configurations
  • Slot time divisibility
  • Bookable day range validation
  • Type validation with allowed values

These tests will help ensure the validation logic is working as expected.


1540-1637: Edge case testing for weekday-specific conflicts.

The tests for weekday-specific conflicts ensure that the system correctly identifies conflicts only on the specific weekdays they actually occur, rather than flagging false positives.

zmsapi/src/Zmsapi/AvailabilityUpdate.php (6)

28-37: Improved readability through method extraction.

The readResponse method has been refactored into smaller, focused methods that improve readability and maintainability. This approach makes the code's intent clearer and easier to understand.


39-66: Enhanced input validation.

The input validation has been significantly improved with dedicated methods that check for required fields and appropriate structure. The early validation with clear error messages helps prevent downstream issues and provides better feedback.


342-364: Consider enhancing error handling in writeEntityUpdate.

As noted in a previous review comment, the error handling in writeEntityUpdate could be improved further to provide more specific error messages and better handle edge cases.

     protected function writeEntityUpdate($entity, $resolveReferences): Entity
     {
         $repository = new AvailabilityRepository();
-        $updatedEntity = null;
+        try {
+            if ($entity->id) {
+                $oldEntity = $repository->readEntity($entity->id);
+                if ($oldEntity !== null && $oldEntity->hasId()) {
+                    $this->writeSpontaneousEntity($oldEntity);
+                    return $repository->updateEntity($entity->id, $entity, $resolveReferences);
+                }
+                throw new AvailabilityUpdateFailed("Entity with ID {$entity->id} not found.");
+            }
 
-        if ($entity->id) {
-            $oldEntity = $repository->readEntity($entity->id);
-            if ($oldEntity !== null && $oldEntity->hasId()) {
-                $this->writeSpontaneousEntity($oldEntity);
-                $updatedEntity = $repository->updateEntity($entity->id, $entity, $resolveReferences);
-            } else {
-                throw new AvailabilityUpdateFailed("Entity with ID {$entity->id} not found.");
-            }
-        } else {
-            $updatedEntity = $repository->writeEntity($entity, $resolveReferences);
-        }
-
-        if (!$updatedEntity) {
-            throw new AvailabilityUpdateFailed();
+            $updatedEntity = $repository->writeEntity($entity, $resolveReferences);
+            if (!$updatedEntity) {
+                throw new AvailabilityUpdateFailed("Failed to create new entity.");
+            }
+            return $updatedEntity;
+        } catch (\Exception $e) {
+            throw new AvailabilityUpdateFailed(
+                "Failed to update entity: " . $e->getMessage(),
+                $e->getCode(),
+                $e
+            );
         }
-
-        return $updatedEntity;
     }

356-356: Consider using $resolveReferences param for new entries here as well.

Line 356 forcibly sets references to "2" for new entities, which may be inconsistent if $resolveReferences was a different number.

-            $updatedEntity = $repository->writeEntity($entity, 2);
+            $updatedEntity = $repository->writeEntity($entity, $resolveReferences);

178-201: Well-structured validation for individual availabilities.

The validateSingleAvailability method effectively handles the conversion of timestamps to DateTime objects and implements thorough validation checks. The structure is clear and the logic is easy to follow.


251-313: Efficient conflict filtering by weekday.

The weekday-specific conflict filtering logic is well implemented, ensuring that only conflicts that actually affect the selected weekday are reported, reducing false positives and providing better user experience.

Thomas Fink added 2 commits February 26, 2025 20:33
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🧹 Nitpick comments (8)
zmsentities/src/Zmsentities/Collection/AvailabilityList.php (3)

313-339: Consider simplifying message formatting in createConflictMessage method.

The current implementation uses HTML entities for spacing ( ) which makes the code less readable and may cause issues if the output context changes.

    private function createConflictMessage(
        Availability $availability1,
        Availability $availability2,
        bool $bothAreNew,
        bool $isEqual
    ): string {
        $dateRange1 = date('d.m.Y', $availability1->startDate) . ' - ' . date('d.m.Y', $availability1->endDate);
        $dateRange2 = date('d.m.Y', $availability2->startDate) . ' - ' . date('d.m.Y', $availability2->endDate);
        $timeRange1 = date('H:i', strtotime($availability1->startTime)) . ' - ' . date('H:i', strtotime($availability1->endTime));
        $timeRange2 = date('H:i', strtotime($availability2->startTime)) . ' - ' . date('H:i', strtotime($availability2->endTime));

        if ($isEqual) {
            $message = "Konflikt: Zwei Öffnungszeiten sind gleich.\n";
        } else {
            $message = "Konflikt: Zwei Öffnungszeiten überschneiden sich.\n";
        }

        if ($bothAreNew) {
-           $message .= "Neue Öffnungszeit:&thinsp;&thinsp;&thinsp;&thinsp;&thinsp;&thinsp;&thinsp;&thinsp;&thinsp;&thinsp;&thinsp;&thinsp;&thinsp;&thinsp;&thinsp;&thinsp;&thinsp;[$dateRange1, $timeRange1, Wochentag(e): " . $availability1->getWeekdayNames() . "]\n"
-               . "Neue Öffnungszeit:&thinsp;&thinsp;&thinsp;&thinsp;&thinsp;&thinsp;&thinsp;&thinsp;&thinsp;&thinsp;&thinsp;&thinsp;&thinsp;&thinsp;&thinsp;&thinsp;&thinsp;[$dateRange2, $timeRange2, Wochentag(e): " . $availability2->getWeekdayNames() . "]";
+           $message .= sprintf("Neue Öffnungszeit:                 [%s, %s, Wochentag(e): %s]\n", 
+               $dateRange1, $timeRange1, $availability1->getWeekdayNames());
+           $message .= sprintf("Neue Öffnungszeit:                 [%s, %s, Wochentag(e): %s]", 
+               $dateRange2, $timeRange2, $availability2->getWeekdayNames());
        } else {
-           $message .= "Bestehende Öffnungszeit:&thinsp;&thinsp;[$dateRange2, $timeRange2, Wochentag(e): " . $availability2->getWeekdayNames() . "]\n"
-               . "Neue Öffnungszeit:&thinsp;&thinsp;&thinsp;&thinsp;&thinsp;&thinsp;&thinsp;&thinsp;&thinsp;&thinsp;&thinsp;&thinsp;&thinsp;&thinsp;&thinsp;&thinsp;&thinsp;[$dateRange1, $timeRange1, Wochentag(e): " . $availability1->getWeekdayNames() . "]";
+           $message .= sprintf("Bestehende Öffnungszeit:           [%s, %s, Wochentag(e): %s]\n", 
+               $dateRange2, $timeRange2, $availability2->getWeekdayNames());
+           $message .= sprintf("Neue Öffnungszeit:                 [%s, %s, Wochentag(e): %s]", 
+               $dateRange1, $timeRange1, $availability1->getWeekdayNames());
        }

        return $message;
    }

365-365: Fix date modification spacing in checkForConflictsWithExistingAvailabilities method.

The date modification string is missing a space, which could lead to unexpected behavior.

-               $currentDate = $currentDate->modify('+1day');
+               $currentDate = $currentDate->modify('+1 day');

419-449: Improve handling of scope objects and arrays in shouldCompareForConflicts method.

The current implementation uses complex nested conditionals and direct array access, which could be simplified and made more robust.

    protected function shouldCompareForConflicts(
        Availability $availability1,
        Availability $availability2
    ): bool {
        // Skip if they're not the same type
        if (isset($availability1->type) && isset($availability2->type) && $availability1->type != $availability2->type) {
            return false;
        }

        // Skip if they're not for the same scope (only if both have scope defined)
        if (isset($availability1->scope) && isset($availability2->scope)) {
-           $scope1Id = is_array($availability1->scope) ? ($availability1->scope['id'] ?? null) : ($availability1->scope->id ?? null);
-           $scope2Id = is_array($availability2->scope) ? ($availability2->scope['id'] ?? null) : ($availability2->scope->id ?? null);
+           $scope1Id = $this->getScopeId($availability1->scope);
+           $scope2Id = $this->getScopeId($availability2->scope);

            if ($scope1Id != $scope2Id) {
                return false;
            }
        }

        // Skip if they're part of the same series
        if (
            (isset($availability1->id) && isset($availability2->id) &&
                $availability1->id === $availability2->id) ||
            (isset($availability1->tempId) && isset($availability2->tempId) &&
                $availability1->tempId === $availability2->tempId)
        ) {
            return false;
        }

        return true;
    }
+    
+    private function getScopeId($scope) {
+        if (is_array($scope)) {
+            return $scope['id'] ?? null;
+        } elseif (is_object($scope)) {
+            return $scope->id ?? null;
+        }
+        return null;
+    }
zmsapi/src/Zmsapi/AvailabilityAdd.php (2)

33-41: Consider adding explicit transaction handling.

While obtaining the write connection here is appropriate, it may be safer to wrap the subsequent insert/update logic in a transaction to prevent partial updates if any conflict or exception occurs in the middle of the process.

Would you like me to propose a code snippet for transaction management?


302-311: Opportunity to unify with AvailabilityUpdate logic.

The updateEntities method is very similar to the one in AvailabilityUpdate. Consider extracting this shared logic into a common helper or base class to adhere to DRY principles.

zmsadmin/src/Zmsadmin/AvailabilityConflicts.php (1)

116-141: Optimize for large scopes with many availabilities.

While the new filtering logic is correct, consider assessing performance if large scopes contain thousands of availabilities. You may want to pre-filter or index them before this loop.

zmsapi/src/Zmsapi/AvailabilityUpdate.php (2)

29-37: Consider adding explicit transaction handling.

Similar to AvailabilityAdd, you might want to wrap creation/updates in a transaction to avoid partial state if an exception is thrown midway.


341-363: Refactor repeated 'writeEntityUpdate' logic.

Both AvailabilityAdd and AvailabilityUpdate contain near-identical entity writing logic, including the spontaneous entity step and reference checks. Consider extracting this to a shared helper method or base class for maintenance and consistency.

📜 Review details

Configuration used: .coderabbit.yaml
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between a7f93b7 and 7e98a91.

📒 Files selected for processing (4)
  • zmsadmin/src/Zmsadmin/AvailabilityConflicts.php (3 hunks)
  • zmsapi/src/Zmsapi/AvailabilityAdd.php (1 hunks)
  • zmsapi/src/Zmsapi/AvailabilityUpdate.php (1 hunks)
  • zmsentities/src/Zmsentities/Collection/AvailabilityList.php (5 hunks)
🧰 Additional context used
📓 Path-based instructions (1)
`**/*.php`: Flag any usage of error_log() as it should be re...

**/*.php: Flag any usage of error_log() as it should be replaced with proper logging mechanisms:

  1. For error handling: Use a proper logging framework with error levels (PSR-3 LoggerInterface)
  2. For debugging: Use a dedicated debug logger or remove debug statements
  3. For CLI output: Use a CLI output handler or symfony/console
  4. For application events: Use structured logging with proper log levels

Example replacement:

// Instead of:
error_log("Import failed - " . $e->getMessage());

// Use:
$logger->error("Import failed", ['error' => $e->getMessage()]);
  • zmsapi/src/Zmsapi/AvailabilityAdd.php
  • zmsadmin/src/Zmsadmin/AvailabilityConflicts.php
  • zmsentities/src/Zmsentities/Collection/AvailabilityList.php
  • zmsapi/src/Zmsapi/AvailabilityUpdate.php
🧠 Learnings (2)
zmsadmin/src/Zmsadmin/AvailabilityConflicts.php (2)
Learnt from: ThomasAFink
PR: it-at-m/eappointment#0
File: :0-0
Timestamp: 2025-02-17T09:47:44.330Z
Learning: The getAvailabilityData() method in AvailabilityConflicts.php should be refactored into smaller methods: validateInput(), processAvailabilityKinds(), getConflictList(), and filterAndSortConflicts() to improve maintainability and reduce complexity.
Learnt from: ThomasAFink
PR: it-at-m/eappointment#0
File: :0-0
Timestamp: 2025-02-17T09:49:29.414Z
Learning: The getAvailabilityData() method in AvailabilityConflicts.php should be refactored to reduce complexity by:
1. Extracting validation logic into separate methods
2. Breaking down availability kind processing
3. Isolating conflict filtering logic
4. Moving sorting to dedicated methods
5. Using early returns to avoid nested conditions
zmsapi/src/Zmsapi/AvailabilityUpdate.php (1)
Learnt from: ThomasAFink
PR: it-at-m/eappointment#0
File: :0-0
Timestamp: 2025-02-17T10:19:41.023Z
Learning: The readResponse() method in AvailabilityUpdate.php should be refactored to reduce complexity by:
1. Separating input validation into dedicated methods
2. Breaking down collection processing logic
3. Isolating conflict detection and filtering
4. Moving entity update logic to separate methods
5. Using type hints for better code clarity
⏰ Context from checks skipped due to timeout of 90000ms (16)
  • GitHub Check: call-owasp-security-checks / security-scan (zmsclient, 8.0)
  • GitHub Check: call-owasp-security-checks / security-scan (zmsdb, 8.0)
  • GitHub Check: call-owasp-security-checks / security-scan (zmsslim, 8.0)
  • GitHub Check: call-owasp-security-checks / security-scan (zmsentities, 8.0)
  • GitHub Check: call-owasp-security-checks / security-scan (zmsdldb, 8.0)
  • GitHub Check: call-owasp-security-checks / security-scan (zmsticketprinter, 8.0)
  • GitHub Check: call-owasp-security-checks / security-scan (zmsstatistic, 8.0)
  • GitHub Check: call-owasp-security-checks / security-scan (zmsmessaging, 8.0)
  • GitHub Check: call-owasp-security-checks / security-scan (zmscitizenapi, 8.0)
  • GitHub Check: call-owasp-security-checks / security-scan (zmscalldisplay, 8.0)
  • GitHub Check: call-php-unit-tests / zmsapi-test
  • GitHub Check: call-owasp-security-checks / security-scan (zmsapi, 8.0)
  • GitHub Check: call-owasp-security-checks / security-scan (zmsadmin, 8.0)
  • GitHub Check: call-php-unit-tests / zmsdb-test
  • GitHub Check: Analyze (python)
  • GitHub Check: Analyze (javascript-typescript)
🔇 Additional comments (7)
zmsentities/src/Zmsentities/Collection/AvailabilityList.php (4)

166-194: LGTM - Comprehensive validation framework.

The new validateInputs method provides a structured approach to validating multiple aspects of availability data.


258-311: LGTM - Well-structured conflict detection.

The method effectively identifies conflicts between new availabilities by comparing each pair and generating detailed conflict objects.


341-370: LGTM - Comprehensive conflict detection with existing availabilities.

This method appropriately checks for conflicts with existing availabilities over a specific date range.


372-417: LGTM - Well-structured overlap detection.

The findOverlapsOnDate method efficiently identifies overlaps on a specific date and generates appropriate conflict objects.

zmsadmin/src/Zmsadmin/AvailabilityConflicts.php (2)

29-47: Great job decomposing the large method into smaller steps.

This separation aligns with the retrieved learnings about reducing complexity in getAvailabilityData. The new helper methods improve clarity and maintainability.


222-235: Docblock justification is valid.

Your suppression of PHPMD for sortConflictedList is appropriate since it’s used as a usort callback. The docblock clarifies this indirect usage well.

zmsapi/src/Zmsapi/AvailabilityUpdate.php (1)

83-93:

❓ Verification inconclusive

Guard against missing 'bookable' indices in updated entities.

As in AvailabilityAdd, there is a potential undefined index risk when accessing $availability->bookable['startInDays'] or ['endInDays'].

Please confirm that testValid() or another check ensures these fields are always set.


🏁 Script executed:

#!/bin/bash
# Search for bookable references in this file
rg -A 2 "bookable\['"

Length of output: 4573


Ensure that updated entities have the required 'bookable' indices defined.

It’s important to guard against missing bookable['startInDays'] and bookable['endInDays'] values in updated entities—as done in AvailabilityAdd—because undefined indices could lead to runtime errors. In this block, an Entity is created and testValid() is called immediately. Please verify that Entity::testValid() or a related validation routine robustly confirms that these bookable keys are present before any downstream access.

  • Confirm that the validation (e.g., within testValid()) enforces the presence of bookable['startInDays'] and bookable['endInDays'].
  • If the validation is missing or insufficient, consider adding explicit guards to prevent undefined index errors in this code path.

Comment on lines +451 to +472
protected function doAvailabilitiesOverlap(
Availability $availability1,
Availability $availability2
): bool {
// Check date overlap
$date1Start = (new \DateTimeImmutable())->setTimestamp($availability1->startDate);
$date1End = (new \DateTimeImmutable())->setTimestamp($availability1->endDate);
$date2Start = (new \DateTimeImmutable())->setTimestamp($availability2->startDate);
$date2End = (new \DateTimeImmutable())->setTimestamp($availability2->endDate);

if (!($date1Start <= $date2End && $date2Start <= $date1End)) {
return false;
}

// Check time overlap
$time1Start = strtotime($availability1->startTime);
$time1End = strtotime($availability1->endTime);
$time2Start = strtotime($availability2->startTime);
$time2End = strtotime($availability2->endTime);

return ($time1Start < $time2End && $time2Start < $time1End);
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Add timezone handling to DateTime creation in doAvailabilitiesOverlap method.

The method creates DateTimeImmutable objects without specifying a timezone, which could lead to inconsistent behavior across different server configurations.

    protected function doAvailabilitiesOverlap(
        Availability $availability1,
        Availability $availability2
    ): bool {
        // Check date overlap
-       $date1Start = (new \DateTimeImmutable())->setTimestamp($availability1->startDate);
-       $date1End = (new \DateTimeImmutable())->setTimestamp($availability1->endDate);
-       $date2Start = (new \DateTimeImmutable())->setTimestamp($availability2->startDate);
-       $date2End = (new \DateTimeImmutable())->setTimestamp($availability2->endDate);
+       $timezone = new \DateTimeZone(date_default_timezone_get());
+       $date1Start = (new \DateTimeImmutable('now', $timezone))->setTimestamp($availability1->startDate);
+       $date1End = (new \DateTimeImmutable('now', $timezone))->setTimestamp($availability1->endDate);
+       $date2Start = (new \DateTimeImmutable('now', $timezone))->setTimestamp($availability2->startDate);
+       $date2End = (new \DateTimeImmutable('now', $timezone))->setTimestamp($availability2->endDate);

        if (!($date1Start <= $date2End && $date2Start <= $date1End)) {
            return false;
        }

        // Check time overlap
        $time1Start = strtotime($availability1->startTime);
        $time1End = strtotime($availability1->endTime);
        $time2Start = strtotime($availability2->startTime);
        $time2End = strtotime($availability2->endTime);

        return ($time1Start < $time2End && $time2Start < $time1End);
    }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
protected function doAvailabilitiesOverlap(
Availability $availability1,
Availability $availability2
): bool {
// Check date overlap
$date1Start = (new \DateTimeImmutable())->setTimestamp($availability1->startDate);
$date1End = (new \DateTimeImmutable())->setTimestamp($availability1->endDate);
$date2Start = (new \DateTimeImmutable())->setTimestamp($availability2->startDate);
$date2End = (new \DateTimeImmutable())->setTimestamp($availability2->endDate);
if (!($date1Start <= $date2End && $date2Start <= $date1End)) {
return false;
}
// Check time overlap
$time1Start = strtotime($availability1->startTime);
$time1End = strtotime($availability1->endTime);
$time2Start = strtotime($availability2->startTime);
$time2End = strtotime($availability2->endTime);
return ($time1Start < $time2End && $time2Start < $time1End);
}
protected function doAvailabilitiesOverlap(
Availability $availability1,
Availability $availability2
): bool {
// Check date overlap
$timezone = new \DateTimeZone(date_default_timezone_get());
$date1Start = (new \DateTimeImmutable('now', $timezone))->setTimestamp($availability1->startDate);
$date1End = (new \DateTimeImmutable('now', $timezone))->setTimestamp($availability1->endDate);
$date2Start = (new \DateTimeImmutable('now', $timezone))->setTimestamp($availability2->startDate);
$date2End = (new \DateTimeImmutable('now', $timezone))->setTimestamp($availability2->endDate);
if (!($date1Start <= $date2End && $date2Start <= $date1End)) {
return false;
}
// Check time overlap
$time1Start = strtotime($availability1->startTime);
$time1End = strtotime($availability1->endTime);
$time2Start = strtotime($availability2->startTime);
$time2End = strtotime($availability2->endTime);
return ($time1Start < $time2End && $time2Start < $time1End);
}

Comment on lines +196 to 216
public function getDateTimeRangeFromList(): array
{
$processList = new ProcessList();
foreach ($this as $availabilityCompare) {
if ($availabilityCompare->isOpenedOnDate($currentDate)) {
$overlaps = $availability->getTimeOverlaps($availabilityCompare, $currentDate);
$processList->addList($overlaps);
$startDateTime = null;
$endDateTime = null;

foreach ($this as $availability) {
$availabilityStartDateTime = (new \DateTimeImmutable())
->setTimestamp($availability->startDate);
$availabilityEndDateTime = (new \DateTimeImmutable())
->setTimestamp($availability->endDate);

if ($startDateTime === null || $availabilityStartDateTime < $startDateTime) {
$startDateTime = $availabilityStartDateTime;
}
if ($endDateTime === null || $availabilityEndDateTime > $endDateTime) {
$endDateTime = $availabilityEndDateTime;
}
}
return $processList;

return [$startDateTime, $endDateTime];
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Improve timezone handling in getDateTimeRangeFromList method.

The method creates DateTimeImmutable objects without specifying a timezone, which could lead to inconsistent behavior across different server configurations.

    public function getDateTimeRangeFromList(): array
    {
        $startDateTime = null;
        $endDateTime = null;
+       $timezone = new \DateTimeZone(date_default_timezone_get());

        foreach ($this as $availability) {
-           $availabilityStartDateTime = (new \DateTimeImmutable())
+           $availabilityStartDateTime = (new \DateTimeImmutable('now', $timezone))
                ->setTimestamp($availability->startDate);
-           $availabilityEndDateTime = (new \DateTimeImmutable())
+           $availabilityEndDateTime = (new \DateTimeImmutable('now', $timezone))
                ->setTimestamp($availability->endDate);

            if ($startDateTime === null || $availabilityStartDateTime < $startDateTime) {
                $startDateTime = $availabilityStartDateTime;
            }
            if ($endDateTime === null || $availabilityEndDateTime > $endDateTime) {
                $endDateTime = $availabilityEndDateTime;
            }
        }

        return [$startDateTime, $endDateTime];
    }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
public function getDateTimeRangeFromList(): array
{
$processList = new ProcessList();
foreach ($this as $availabilityCompare) {
if ($availabilityCompare->isOpenedOnDate($currentDate)) {
$overlaps = $availability->getTimeOverlaps($availabilityCompare, $currentDate);
$processList->addList($overlaps);
$startDateTime = null;
$endDateTime = null;
foreach ($this as $availability) {
$availabilityStartDateTime = (new \DateTimeImmutable())
->setTimestamp($availability->startDate);
$availabilityEndDateTime = (new \DateTimeImmutable())
->setTimestamp($availability->endDate);
if ($startDateTime === null || $availabilityStartDateTime < $startDateTime) {
$startDateTime = $availabilityStartDateTime;
}
if ($endDateTime === null || $availabilityEndDateTime > $endDateTime) {
$endDateTime = $availabilityEndDateTime;
}
}
return $processList;
return [$startDateTime, $endDateTime];
}
public function getDateTimeRangeFromList(): array
{
$startDateTime = null;
$endDateTime = null;
$timezone = new \DateTimeZone(date_default_timezone_get());
foreach ($this as $availability) {
$availabilityStartDateTime = (new \DateTimeImmutable('now', $timezone))
->setTimestamp($availability->startDate);
$availabilityEndDateTime = (new \DateTimeImmutable('now', $timezone))
->setTimestamp($availability->endDate);
if ($startDateTime === null || $availabilityStartDateTime < $startDateTime) {
$startDateTime = $availabilityStartDateTime;
}
if ($endDateTime === null || $availabilityEndDateTime > $endDateTime) {
$endDateTime = $availabilityEndDateTime;
}
}
return [$startDateTime, $endDateTime];
}

Comment on lines +166 to +189
private function validateSingleAvailability(
Entity $availability,
Collection $mergedCollection,
\DateTimeImmutable $selectedDate
): array {
$startDate = (new \DateTimeImmutable())->setTimestamp($availability->startDate);
$endDate = (new \DateTimeImmutable())->setTimestamp($availability->endDate);
$startDateTime = new \DateTimeImmutable(
"{$startDate->format('Y-m-d')} {$availability->startTime}"
);
$endDateTime = new \DateTimeImmutable(
"{$endDate->format('Y-m-d')} {$availability->endTime}"
);

return $mergedCollection->validateInputs(
$startDateTime,
$endDateTime,
$selectedDate,
$availability->kind ?? 'default',
$availability->bookable['startInDays'],
$availability->bookable['endInDays'],
$availability->weekday
);
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Verification agent

🧩 Analysis chain

Guard against missing 'bookable' indices.

The code references $availability->bookable['startInDays'] and $availability->bookable['endInDays'], but there's no check ensuring bookable keys exist. This could cause undefined index issues.

Please verify that these keys are indeed guaranteed by upstream validation (e.g., testValid() or other checks). Otherwise, add fallback logic or validations to prevent runtime notices.


🏁 Script executed:

#!/bin/bash
# Search for places where "bookable['" is used without prior existence checks
rg -A 2 "bookable\['" 

Length of output: 4573


Guard Missing 'bookable' Indices in validateSingleAvailability

  • In zmsapi/src/Zmsapi/AvailabilityAdd.php (lines 166–189), the method directly accesses $availability->bookable['startInDays'] and $availability->bookable['endInDays'] without checking for their existence.
  • A search across the repository (including AvailabilityUpdate.php and related test files) shows that these keys are used without preliminary checks, thereby exposing the risk of undefined index errors.
  • Action Required: Ensure that the keys are either guaranteed by upstream validations (such as in a testValid() method) or incorporate fallback logic. For example, you might use the null coalescing operator:
    $startInDays = $availability->bookable['startInDays'] ?? 0;
    $endInDays = $availability->bookable['endInDays'] ?? 0;
    ...or add explicit validations before usage.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants