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 #817

Conversation

ThomasAFink
Copy link
Member

@ThomasAFink ThomasAFink commented Feb 4, 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
    • Enhanced scheduling forms now display clear, formatted error messages and auto-hide save notifications.
    • New validations prevent overlapping or conflicting availability entries.
    • Introduced new error handling messages for failed availability additions and updates.
    • Updated UI for error display, including improved accessibility and clarity.
    • New prop added to footer buttons for better error handling and availability checks.
    • Added new properties for better error reporting and validation checks in various components.
    • Reduced default appointment slot duration from 12 to 10 minutes.
    • Improved handling of conflict messages and error states in various components.
  • Bug Fixes
    • Adjusted time selection limits and appointment slot calculations for improved scheduling accuracy.
    • Fixed issues with error handling related to time validation and conflict detection.
    • Resolved rendering issues for conflict messages.
  • Refactor
    • Streamlined conflict detection and input validation for a smoother user experience.
    • Improved the structure and clarity of error messages and validation logic.
    • Updated component structures for better readability and maintenance.
  • Tests
    • Expanded test scenarios to ensure robust handling of various scheduling and validation conditions.
    • Added tests for overlapping availability entries and validation edge cases.

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

coderabbitai bot commented Feb 6, 2025

Tip

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

@ThomasAFink Here are both complete test methods with weekday support:

public function testDuplicateOverlappingAvailability()
{
    $input = (new Entity)->createExample();
    $currentTimestamp = time();
    $testDate = $currentTimestamp + (20 * 24 * 60 * 60);
    $weekday = (int)date('N', $testDate);
    
    $input['startDate'] = $testDate;
    $input['endDate'] = $currentTimestamp + (50 * 24 * 60 * 60);
    $input['startTime'] = "09:00:00";
    $input['endTime'] = "17:00:00";
    $input['weekday'] = array_combine(
        ['sunday', 'monday', 'tuesday', 'wednesday', 'thursday', 'friday', 'saturday'],
        array_map(function($i) use ($weekday) { return $i === $weekday ? '4' : '0'; }, range(1, 7))
    );
    $input['scope'] = [
        "id" => 312,
        "dayoff" => [
            [
                "id" => 35,
                "date" => $currentTimestamp + (70 * 24 * 60 * 60),
                "name" => "1. Mai",
                "lastChange" => $currentTimestamp
            ],
            [
                "id" => 36,
                "date" => $currentTimestamp + (140 * 24 * 60 * 60),
                "name" => "Christi Himmelfahrt",
                "lastChange" => $currentTimestamp
            ]
        ]
    ];
    $input['kind'] = "default";

    $entity = (new Query())->writeEntity($input);
    $secondEntity = (new Query())->writeEntity($input);
    $this->setWorkstation();
    $this->expectException(AvailabilityUpdateFailed::class);

    $this->render([], [
        '__body' => json_encode([
            'availabilityList' => [
                [
                    "id" => $entity->getId(),
                    "description" => "Duplicate Entry 1",
                    "startDate" => $testDate,
                    "endDate" => $testDate,
                    "startTime" => "09:00:00",
                    "endTime" => "17:00:00",
                    "kind" => "default",
                    "weekday" => array_combine(
                        ['sunday', 'monday', 'tuesday', 'wednesday', 'thursday', 'friday', 'saturday'],
                        array_map(function($i) use ($weekday) { return $i === $weekday ? '4' : '0'; }, range(1, 7))
                    ),
                    "scope" => [
                        "id" => 312,
                        "dayoff" => [
                            [
                                "id" => 35,
                                "date" => $currentTimestamp + (70 * 24 * 60 * 60),
                                "name" => "1. Mai",
                                "lastChange" => $currentTimestamp
                            ],
                            [
                                "id" => 36,
                                "date" => $currentTimestamp + (140 * 24 * 60 * 60),
                                "name" => "Christi Himmelfahrt",
                                "lastChange" => $currentTimestamp
                            ]
                        ]
                    ]
                ],
                [
                    "id" => $secondEntity->getId(),
                    "description" => "Duplicate Entry 2",
                    "startDate" => $testDate,
                    "endDate" => $testDate,
                    "startTime" => "09:00:00",
                    "endTime" => "17:00:00",
                    "kind" => "default",
                    "weekday" => array_combine(
                        ['sunday', 'monday', 'tuesday', 'wednesday', 'thursday', 'friday', 'saturday'],
                        array_map(function($i) use ($weekday) { return $i === $weekday ? '4' : '0'; }, range(1, 7))
                    ),
                    "scope" => [
                        "id" => 312,
                        "dayoff" => [
                            [
                                "id" => 35,
                                "date" => $currentTimestamp + (70 * 24 * 60 * 60),
                                "name" => "1. Mai",
                                "lastChange" => $currentTimestamp
                            ],
                            [
                                "id" => 36,
                                "date" => $currentTimestamp + (140 * 24 * 60 * 60),
                                "name" => "Christi Himmelfahrt",
                                "lastChange" => $currentTimestamp
                            ]
                        ]
                    ]
                ]
            ],
            'selectedDate' => date('Y-m-d', $testDate)
        ])
    ], []);
}

public function testOverlappingAvailability()
{
    $input = (new Entity)->createExample();
    $currentTimestamp = time();
    $testDate = $currentTimestamp + (20 * 24 * 60 * 60);
    $weekday = (int)date('N', $testDate);

    $input['startDate'] = $testDate;
    $input['endDate'] = $currentTimestamp + (50 * 24 * 60 * 60);
    $input['startTime'] = "09:00:00";
    $input['endTime'] = "17:00:00";
    $input['weekday'] = array_combine(
        ['sunday', 'monday', 'tuesday', 'wednesday', 'thursday', 'friday', 'saturday'],
        array_map(function($i) use ($weekday) { return $i === $weekday ? '4' : '0'; }, range(1, 7))
    );
    $input['scope'] = [
        "id" => 312,
        "dayoff" => [
            [
                "id" => 35,
                "date" => $currentTimestamp + (70 * 24 * 60 * 60),
                "name" => "1. Mai",
                "lastChange" => $currentTimestamp
            ],
            [
                "id" => 36,
                "date" => $currentTimestamp + (140 * 24 * 60 * 60),
                "name" => "Christi Himmelfahrt",
                "lastChange" => $currentTimestamp
            ]
        ]
    ];
    $input['kind'] = "default";

    $secondInput = (new Entity)->createExample();
    $secondInput['startDate'] = $testDate;
    $secondInput['endDate'] = $currentTimestamp + (50 * 24 * 60 * 60);
    $secondInput['startTime'] = "10:00:00";
    $secondInput['endTime'] = "20:00:00";
    $secondInput['weekday'] = array_combine(
        ['sunday', 'monday', 'tuesday', 'wednesday', 'thursday', 'friday', 'saturday'],
        array_map(function($i) use ($weekday) { return $i === $weekday ? '4' : '0'; }, range(1, 7))
    );
    $secondInput['scope'] = $input['scope'];
    $secondInput['kind'] = "default";

    $entity = (new Query())->writeEntity($input);
    $secondEntity = (new Query())->writeEntity($secondInput);
    $this->setWorkstation();
    $this->expectException(AvailabilityUpdateFailed::class);

    $this->render([], [
        '__body' => json_encode([
            'availabilityList' => [
                [
                    "id" => $entity->getId(),
                    "description" => "Overlapping Entry 1",
                    "startDate" => $testDate,
                    "endDate" => $testDate,
                    "startTime" => "09:00:00",
                    "endTime" => "17:00:00",
                    "kind" => "default",
                    "weekday" => array_combine(
                        ['sunday', 'monday', 'tuesday', 'wednesday', 'thursday', 'friday', 'saturday'],
                        array_map(function($i) use ($weekday) { return $i === $weekday ? '4' : '0'; }, range(1, 7))
                    ),
                    "scope" => [
                        "id" => 312,
                        "dayoff" => [
                            [
                                "id" => 35,
                                "date" => $currentTimestamp + (70 * 24 * 60 * 60),
                                "name" => "1. Mai",
                                "lastChange" => $currentTimestamp
                            ],
                            [
                                "id" => 36,
                                "date" => $currentTimestamp + (140 * 24 * 60 * 60),
                                "name" => "Christi Himmelfahrt",
                                "lastChange" => $currentTimestamp
                            ]
                        ]
                    ]
                ],
                [
                    "id" => $secondEntity->getId(),
                    "description" => "Overlapping Entry 2",
                    "startDate" => $testDate,
                    "endDate" => $testDate,
                    "startTime" => "10:00:00",
                    "endTime" => "20:00:00",
                    "kind" => "default",
                    "weekday" => array_combine(
                        ['sunday', 'monday', 'tuesday', 'wednesday', 'thursday', 'friday', 'saturday'],
                        array_map(function($i) use ($weekday) { return $i === $weekday ? '4' : '0'; }, range(1, 7))
                    ),
                    "scope" => [
                        "id" => 312,
                        "dayoff" => [
                            [
                                "id" => 35,
                                "date" => $currentTimestamp + (70 * 24 * 60 * 60),
                                "name" => "1. Mai",
                                "lastChange" => $currentTimestamp
                            ],
                            [
                                "id" => 36,
                                "date" => $currentTimestamp + (140 * 24 * 60 * 60),
                                "name" => "Christi Himmelfahrt",
                                "lastChange" => $currentTimestamp
                            ]
                        ]
                    ]
                ]
            ],
            'selectedDate' => date('Y-m-d', $testDate)
        ])
    ], []);
}

Key changes:

  1. Added weekday data to both input and test entities
  2. Set weekday value (4) for the current weekday
  3. Set selectedDate to match the test date
  4. Kept existing dayoff data and test structure
  5. Simplified date handling by using a single testDate

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

Timestamp: 2025-02-05T19:05:23.456Z
Learning: When writing availability update tests, ensure both input and test entities have matching weekday data that corresponds to the test date, and set selectedDate to match the test date to properly test weekday-based conflict detection.

🧠 Learnings used
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.

@ThomasAFink
Copy link
Member Author

Tip

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

@ThomasAFink Here are both complete test methods with weekday support:

public function testDuplicateOverlappingAvailability()
{
    $input = (new Entity)->createExample();
    $currentTimestamp = time();
    $testDate = $currentTimestamp + (20 * 24 * 60 * 60);
    $weekday = (int)date('N', $testDate);
    
    $input['startDate'] = $testDate;
    $input['endDate'] = $currentTimestamp + (50 * 24 * 60 * 60);
    $input['startTime'] = "09:00:00";
    $input['endTime'] = "17:00:00";
    $input['weekday'] = array_combine(
        ['sunday', 'monday', 'tuesday', 'wednesday', 'thursday', 'friday', 'saturday'],
        array_map(function($i) use ($weekday) { return $i === $weekday ? '4' : '0'; }, range(1, 7))
    );
    $input['scope'] = [
        "id" => 312,
        "dayoff" => [
            [
                "id" => 35,
                "date" => $currentTimestamp + (70 * 24 * 60 * 60),
                "name" => "1. Mai",
                "lastChange" => $currentTimestamp
            ],
            [
                "id" => 36,
                "date" => $currentTimestamp + (140 * 24 * 60 * 60),
                "name" => "Christi Himmelfahrt",
                "lastChange" => $currentTimestamp
            ]
        ]
    ];
    $input['kind'] = "default";

    $entity = (new Query())->writeEntity($input);
    $secondEntity = (new Query())->writeEntity($input);
    $this->setWorkstation();
    $this->expectException(AvailabilityUpdateFailed::class);

    $this->render([], [
        '__body' => json_encode([
            'availabilityList' => [
                [
                    "id" => $entity->getId(),
                    "description" => "Duplicate Entry 1",
                    "startDate" => $testDate,
                    "endDate" => $testDate,
                    "startTime" => "09:00:00",
                    "endTime" => "17:00:00",
                    "kind" => "default",
                    "weekday" => array_combine(
                        ['sunday', 'monday', 'tuesday', 'wednesday', 'thursday', 'friday', 'saturday'],
                        array_map(function($i) use ($weekday) { return $i === $weekday ? '4' : '0'; }, range(1, 7))
                    ),
                    "scope" => [
                        "id" => 312,
                        "dayoff" => [
                            [
                                "id" => 35,
                                "date" => $currentTimestamp + (70 * 24 * 60 * 60),
                                "name" => "1. Mai",
                                "lastChange" => $currentTimestamp
                            ],
                            [
                                "id" => 36,
                                "date" => $currentTimestamp + (140 * 24 * 60 * 60),
                                "name" => "Christi Himmelfahrt",
                                "lastChange" => $currentTimestamp
                            ]
                        ]
                    ]
                ],
                [
                    "id" => $secondEntity->getId(),
                    "description" => "Duplicate Entry 2",
                    "startDate" => $testDate,
                    "endDate" => $testDate,
                    "startTime" => "09:00:00",
                    "endTime" => "17:00:00",
                    "kind" => "default",
                    "weekday" => array_combine(
                        ['sunday', 'monday', 'tuesday', 'wednesday', 'thursday', 'friday', 'saturday'],
                        array_map(function($i) use ($weekday) { return $i === $weekday ? '4' : '0'; }, range(1, 7))
                    ),
                    "scope" => [
                        "id" => 312,
                        "dayoff" => [
                            [
                                "id" => 35,
                                "date" => $currentTimestamp + (70 * 24 * 60 * 60),
                                "name" => "1. Mai",
                                "lastChange" => $currentTimestamp
                            ],
                            [
                                "id" => 36,
                                "date" => $currentTimestamp + (140 * 24 * 60 * 60),
                                "name" => "Christi Himmelfahrt",
                                "lastChange" => $currentTimestamp
                            ]
                        ]
                    ]
                ]
            ],
            'selectedDate' => date('Y-m-d', $testDate)
        ])
    ], []);
}

public function testOverlappingAvailability()
{
    $input = (new Entity)->createExample();
    $currentTimestamp = time();
    $testDate = $currentTimestamp + (20 * 24 * 60 * 60);
    $weekday = (int)date('N', $testDate);

    $input['startDate'] = $testDate;
    $input['endDate'] = $currentTimestamp + (50 * 24 * 60 * 60);
    $input['startTime'] = "09:00:00";
    $input['endTime'] = "17:00:00";
    $input['weekday'] = array_combine(
        ['sunday', 'monday', 'tuesday', 'wednesday', 'thursday', 'friday', 'saturday'],
        array_map(function($i) use ($weekday) { return $i === $weekday ? '4' : '0'; }, range(1, 7))
    );
    $input['scope'] = [
        "id" => 312,
        "dayoff" => [
            [
                "id" => 35,
                "date" => $currentTimestamp + (70 * 24 * 60 * 60),
                "name" => "1. Mai",
                "lastChange" => $currentTimestamp
            ],
            [
                "id" => 36,
                "date" => $currentTimestamp + (140 * 24 * 60 * 60),
                "name" => "Christi Himmelfahrt",
                "lastChange" => $currentTimestamp
            ]
        ]
    ];
    $input['kind'] = "default";

    $secondInput = (new Entity)->createExample();
    $secondInput['startDate'] = $testDate;
    $secondInput['endDate'] = $currentTimestamp + (50 * 24 * 60 * 60);
    $secondInput['startTime'] = "10:00:00";
    $secondInput['endTime'] = "20:00:00";
    $secondInput['weekday'] = array_combine(
        ['sunday', 'monday', 'tuesday', 'wednesday', 'thursday', 'friday', 'saturday'],
        array_map(function($i) use ($weekday) { return $i === $weekday ? '4' : '0'; }, range(1, 7))
    );
    $secondInput['scope'] = $input['scope'];
    $secondInput['kind'] = "default";

    $entity = (new Query())->writeEntity($input);
    $secondEntity = (new Query())->writeEntity($secondInput);
    $this->setWorkstation();
    $this->expectException(AvailabilityUpdateFailed::class);

    $this->render([], [
        '__body' => json_encode([
            'availabilityList' => [
                [
                    "id" => $entity->getId(),
                    "description" => "Overlapping Entry 1",
                    "startDate" => $testDate,
                    "endDate" => $testDate,
                    "startTime" => "09:00:00",
                    "endTime" => "17:00:00",
                    "kind" => "default",
                    "weekday" => array_combine(
                        ['sunday', 'monday', 'tuesday', 'wednesday', 'thursday', 'friday', 'saturday'],
                        array_map(function($i) use ($weekday) { return $i === $weekday ? '4' : '0'; }, range(1, 7))
                    ),
                    "scope" => [
                        "id" => 312,
                        "dayoff" => [
                            [
                                "id" => 35,
                                "date" => $currentTimestamp + (70 * 24 * 60 * 60),
                                "name" => "1. Mai",
                                "lastChange" => $currentTimestamp
                            ],
                            [
                                "id" => 36,
                                "date" => $currentTimestamp + (140 * 24 * 60 * 60),
                                "name" => "Christi Himmelfahrt",
                                "lastChange" => $currentTimestamp
                            ]
                        ]
                    ]
                ],
                [
                    "id" => $secondEntity->getId(),
                    "description" => "Overlapping Entry 2",
                    "startDate" => $testDate,
                    "endDate" => $testDate,
                    "startTime" => "10:00:00",
                    "endTime" => "20:00:00",
                    "kind" => "default",
                    "weekday" => array_combine(
                        ['sunday', 'monday', 'tuesday', 'wednesday', 'thursday', 'friday', 'saturday'],
                        array_map(function($i) use ($weekday) { return $i === $weekday ? '4' : '0'; }, range(1, 7))
                    ),
                    "scope" => [
                        "id" => 312,
                        "dayoff" => [
                            [
                                "id" => 35,
                                "date" => $currentTimestamp + (70 * 24 * 60 * 60),
                                "name" => "1. Mai",
                                "lastChange" => $currentTimestamp
                            ],
                            [
                                "id" => 36,
                                "date" => $currentTimestamp + (140 * 24 * 60 * 60),
                                "name" => "Christi Himmelfahrt",
                                "lastChange" => $currentTimestamp
                            ]
                        ]
                    ]
                ]
            ],
            'selectedDate' => date('Y-m-d', $testDate)
        ])
    ], []);
}

Key changes:

  1. Added weekday data to both input and test entities
  2. Set weekday value (4) for the current weekday
  3. Set selectedDate to match the test date
  4. Kept existing dayoff data and test structure
  5. Simplified date handling by using a single testDate

✏️ Learnings added
🧠 Learnings used

@coderabbitai still fails

Time: 00:18.400, Memory: 50,00 MB
There were 2 failures:
1) BO\Zmsapi\Tests\AvailabilityUpdateTest::testDuplicateOverlappingAvailability
Failed asserting that exception of type "BO\Zmsapi\Exception\Availability\AvailabilityUpdateFailed" is thrown.
2) BO\Zmsapi\Tests\AvailabilityUpdateTest::testOverlappingAvailability
Failed asserting that exception of type "BO\Zmsapi\Exception\Availability\AvailabilityUpdateFailed" is thrown.
FAILURES!
Tests: 570, Assertions: 1223, Failures: 2.

Why use similar to AvailabilityAddTest in AvailabilityAddUpdate

AvailabilityAddTest

    public function testOverlappingAvailability()
    {
        $this->setWorkstation();
        $this->expectException(AvailabilityAddFailed::class);
    
        $startDate = time() + (2 * 24 * 60 * 60);
        $weekday = (int)date('N', $startDate);
        $dayoffData = [
            [
                "id" => "302",
                "date" => 1458860400,
                "lastChange" => 1566566540,
                "name" => "Karfreitag"
            ]
        ];
    
        $this->render([], [
            '__body' => json_encode([
                'availabilityList' => [
                    [
                        "id" => 21202,
                        "description" => "Overlapping Entry 1",
                        "startDate" => $startDate,
                        "endDate" => $startDate + (24 * 60 * 60),
                        "startTime" => "09:00:00",
                        "endTime" => "17:00:00",
                        "kind" => "default",
                        "weekday" => array_combine(
                            ['sunday', 'monday', 'tuesday', 'wednesday', 'thursday', 'friday', 'saturday'],
                            array_map(function($i) use ($weekday) { return $i === $weekday ? '4' : '0'; }, range(1, 7))
                        ),
                        "scope" => [
                            "id" => 312,
                            "dayoff" => $dayoffData
                        ]
                    ],
                    [
                        "id" => 21203,
                        "description" => "Overlapping Entry 2",
                        "startDate" => $startDate,
                        "endDate" => $startDate + (24 * 60 * 60),
                        "startTime" => "10:00:00",
                        "endTime" => "18:00:00",
                        "kind" => "default",
                        "weekday" => array_combine(
                            ['sunday', 'monday', 'tuesday', 'wednesday', 'thursday', 'friday', 'saturday'],
                            array_map(function($i) use ($weekday) { return $i === $weekday ? '4' : '0'; }, range(1, 7))
                        ),
                        "scope" => [
                            "id" => 312,
                            "dayoff" => $dayoffData
                        ]
                    ]
                ],
                'selectedDate' => date('Y-m-d', $startDate)
            ])
        ], []);
    }
    
    public function testDuplicateOverlappingAvailability()
    {
        $this->setWorkstation();
        $this->expectException(AvailabilityAddFailed::class);
    
        $startDate = time() + (3 * 24 * 60 * 60);
        $weekday = (int)date('N', $startDate);
        $dayoffData = [
            [
                "id" => "302",
                "date" => 1458860400,
                "lastChange" => 1566566540,
                "name" => "Karfreitag"
            ]
        ];
    
        $this->render([], [
            '__body' => json_encode([
                'availabilityList' => [
                    [
                        "id" => 21202,
                        "description" => "Duplicate Entry 1",
                        "startDate" => $startDate,
                        "endDate" => $startDate + (24 * 60 * 60),
                        "startTime" => "09:00:00",
                        "endTime" => "17:00:00",
                        "kind" => "default",
                        "weekday" => array_combine(
                            ['sunday', 'monday', 'tuesday', 'wednesday', 'thursday', 'friday', 'saturday'],
                            array_map(function($i) use ($weekday) { return $i === $weekday ? '4' : '0'; }, range(1, 7))
                        ),
                        "scope" => [
                            "id" => 312,
                            "dayoff" => $dayoffData
                        ]
                    ],
                    [
                        "id" => 21203,
                        "description" => "Duplicate Entry 2",
                        "startDate" => $startDate,
                        "endDate" => $startDate + (24 * 60 * 60),
                        "startTime" => "09:00:00",
                        "endTime" => "17:00:00",
                        "kind" => "default",
                        "weekday" => array_combine(
                            ['sunday', 'monday', 'tuesday', 'wednesday', 'thursday', 'friday', 'saturday'],
                            array_map(function($i) use ($weekday) { return $i === $weekday ? '4' : '0'; }, range(1, 7))
                        ),
                        "scope" => [
                            "id" => 312,
                            "dayoff" => $dayoffData
                        ]
                    ]
                ],
                'selectedDate' => date('Y-m-d', $startDate)
            ])
        ], []);
    }

AvailabilityUpdateTest

class AvailabilityUpdateTest extends Base
{
    protected $classname = "AvailabilityUpdate";

    public function testRendering()
    {
        $input = (new Entity)->createExample();
        $currentTimestamp = time();
        $input['startDate'] = $currentTimestamp + (2 * 24 * 60 * 60); // 2 days in the future
        $input['endDate'] = $currentTimestamp + (5 * 24 * 60 * 60);   // 5 days in the future
        $input['startTime'] = "09:00:00";
        $input['endTime'] = "17:00:00";
        $input['scope'] = [
            "id" => 312,
            "dayoff" => [
                [
                    "id" => 35,
                    "date" => $currentTimestamp + (7 * 24 * 60 * 60), // 7 days in the future
                    "name" => "1. Mai",
                    "lastChange" => $currentTimestamp
                ],
                [
                    "id" => 36,
                    "date" => $currentTimestamp + (14 * 24 * 60 * 60), // 14 days in the future
                    "name" => "Christi Himmelfahrt",
                    "lastChange" => $currentTimestamp
                ]
            ]
        ];
        $input['kind'] = "default";

        $entity = (new Query())->writeEntity($input);
        $this->setWorkstation();

        $response = $this->render([
            "id" => $entity->getId()
        ], [
            '__body' => json_encode([
                'availabilityList' => [
                    [
                        "id" => $entity->getId(),
                        "description" => "Test Öffnungszeit update",
                        "startDate" => $currentTimestamp + (2 * 24 * 60 * 60),
                        "endDate" => $currentTimestamp + (5 * 24 * 60 * 60),
                        "startTime" => "09:00:00",
                        "endTime" => "17:00:00",
                        "kind" => "default",
                        "scope" => [
                            "id" => 312,
                            "dayoff" => [
                                [
                                    "id" => 35,
                                    "date" => $currentTimestamp + (7 * 24 * 60 * 60),
                                    "name" => "1. Mai",
                                    "lastChange" => $currentTimestamp
                                ],
                                [
                                    "id" => 36,
                                    "date" => $currentTimestamp + (14 * 24 * 60 * 60),
                                    "name" => "Christi Himmelfahrt",
                                    "lastChange" => $currentTimestamp
                                ]
                            ]
                        ]
                    ]
                ],
                'selectedDate' => date('Y-m-d')
            ])
        ], []);

        $this->assertStringContainsString('availability.json', (string) $response->getBody());
        $this->assertTrue(200 == $response->getStatusCode());
    }

    public function testDuplicateOverlappingAvailability()
    {
        $input = (new Entity)->createExample();
        $currentTimestamp = time();
        $testDate = $currentTimestamp + (20 * 24 * 60 * 60);
        $weekday = (int)date('N', $testDate);
        
        $input['startDate'] = $testDate;
        $input['endDate'] = $currentTimestamp + (50 * 24 * 60 * 60);
        $input['startTime'] = "09:00:00";
        $input['endTime'] = "17:00:00";
        $input['weekday'] = array_combine(
            ['sunday', 'monday', 'tuesday', 'wednesday', 'thursday', 'friday', 'saturday'],
            array_map(function($i) use ($weekday) { return $i === $weekday ? '4' : '0'; }, range(1, 7))
        );
        $input['scope'] = [
            "id" => 312,
            "dayoff" => [
                [
                    "id" => 35,
                    "date" => $currentTimestamp + (70 * 24 * 60 * 60),
                    "name" => "1. Mai",
                    "lastChange" => $currentTimestamp
                ],
                [
                    "id" => 36,
                    "date" => $currentTimestamp + (140 * 24 * 60 * 60),
                    "name" => "Christi Himmelfahrt",
                    "lastChange" => $currentTimestamp
                ]
            ]
        ];
        $input['kind'] = "default";
    
        $entity = (new Query())->writeEntity($input);
        $secondEntity = (new Query())->writeEntity($input);
        $this->setWorkstation();
        $this->expectException(AvailabilityUpdateFailed::class);
    
        $this->render([], [
            '__body' => json_encode([
                'availabilityList' => [
                    [
                        "id" => $entity->getId(),
                        "description" => "Duplicate Entry 1",
                        "startDate" => $testDate,
                        "endDate" => $testDate,
                        "startTime" => "09:00:00",
                        "endTime" => "17:00:00",
                        "kind" => "default",
                        "weekday" => array_combine(
                            ['sunday', 'monday', 'tuesday', 'wednesday', 'thursday', 'friday', 'saturday'],
                            array_map(function($i) use ($weekday) { return $i === $weekday ? '4' : '0'; }, range(1, 7))
                        ),
                        "scope" => [
                            "id" => 312,
                            "dayoff" => [
                                [
                                    "id" => 35,
                                    "date" => $currentTimestamp + (70 * 24 * 60 * 60),
                                    "name" => "1. Mai",
                                    "lastChange" => $currentTimestamp
                                ],
                                [
                                    "id" => 36,
                                    "date" => $currentTimestamp + (140 * 24 * 60 * 60),
                                    "name" => "Christi Himmelfahrt",
                                    "lastChange" => $currentTimestamp
                                ]
                            ]
                        ]
                    ],
                    [
                        "id" => $secondEntity->getId(),
                        "description" => "Duplicate Entry 2",
                        "startDate" => $testDate,
                        "endDate" => $testDate,
                        "startTime" => "09:00:00",
                        "endTime" => "17:00:00",
                        "kind" => "default",
                        "weekday" => array_combine(
                            ['sunday', 'monday', 'tuesday', 'wednesday', 'thursday', 'friday', 'saturday'],
                            array_map(function($i) use ($weekday) { return $i === $weekday ? '4' : '0'; }, range(1, 7))
                        ),
                        "scope" => [
                            "id" => 312,
                            "dayoff" => [
                                [
                                    "id" => 35,
                                    "date" => $currentTimestamp + (70 * 24 * 60 * 60),
                                    "name" => "1. Mai",
                                    "lastChange" => $currentTimestamp
                                ],
                                [
                                    "id" => 36,
                                    "date" => $currentTimestamp + (140 * 24 * 60 * 60),
                                    "name" => "Christi Himmelfahrt",
                                    "lastChange" => $currentTimestamp
                                ]
                            ]
                        ]
                    ]
                ],
                'selectedDate' => date('Y-m-d', $testDate)
            ])
        ], []);
    }
    
    public function testOverlappingAvailability()
    {
        $input = (new Entity)->createExample();
        $currentTimestamp = time();
        $testDate = $currentTimestamp + (20 * 24 * 60 * 60);
        $weekday = (int)date('N', $testDate);
    
        $input['startDate'] = $testDate;
        $input['endDate'] = $currentTimestamp + (50 * 24 * 60 * 60);
        $input['startTime'] = "09:00:00";
        $input['endTime'] = "17:00:00";
        $input['weekday'] = array_combine(
            ['sunday', 'monday', 'tuesday', 'wednesday', 'thursday', 'friday', 'saturday'],
            array_map(function($i) use ($weekday) { return $i === $weekday ? '4' : '0'; }, range(1, 7))
        );
        $input['scope'] = [
            "id" => 312,
            "dayoff" => [
                [
                    "id" => 35,
                    "date" => $currentTimestamp + (70 * 24 * 60 * 60),
                    "name" => "1. Mai",
                    "lastChange" => $currentTimestamp
                ],
                [
                    "id" => 36,
                    "date" => $currentTimestamp + (140 * 24 * 60 * 60),
                    "name" => "Christi Himmelfahrt",
                    "lastChange" => $currentTimestamp
                ]
            ]
        ];
        $input['kind'] = "default";
    
        $secondInput = (new Entity)->createExample();
        $secondInput['startDate'] = $testDate;
        $secondInput['endDate'] = $currentTimestamp + (50 * 24 * 60 * 60);
        $secondInput['startTime'] = "10:00:00";
        $secondInput['endTime'] = "20:00:00";
        $secondInput['weekday'] = array_combine(
            ['sunday', 'monday', 'tuesday', 'wednesday', 'thursday', 'friday', 'saturday'],
            array_map(function($i) use ($weekday) { return $i === $weekday ? '4' : '0'; }, range(1, 7))
        );
        $secondInput['scope'] = $input['scope'];
        $secondInput['kind'] = "default";
    
        $entity = (new Query())->writeEntity($input);
        $secondEntity = (new Query())->writeEntity($secondInput);
        $this->setWorkstation();
        $this->expectException(AvailabilityUpdateFailed::class);
    
        $this->render([], [
            '__body' => json_encode([
                'availabilityList' => [
                    [
                        "id" => $entity->getId(),
                        "description" => "Overlapping Entry 1",
                        "startDate" => $testDate,
                        "endDate" => $testDate,
                        "startTime" => "09:00:00",
                        "endTime" => "17:00:00",
                        "kind" => "default",
                        "weekday" => array_combine(
                            ['sunday', 'monday', 'tuesday', 'wednesday', 'thursday', 'friday', 'saturday'],
                            array_map(function($i) use ($weekday) { return $i === $weekday ? '4' : '0'; }, range(1, 7))
                        ),
                        "scope" => [
                            "id" => 312,
                            "dayoff" => [
                                [
                                    "id" => 35,
                                    "date" => $currentTimestamp + (70 * 24 * 60 * 60),
                                    "name" => "1. Mai",
                                    "lastChange" => $currentTimestamp
                                ],
                                [
                                    "id" => 36,
                                    "date" => $currentTimestamp + (140 * 24 * 60 * 60),
                                    "name" => "Christi Himmelfahrt",
                                    "lastChange" => $currentTimestamp
                                ]
                            ]
                        ]
                    ],
                    [
                        "id" => $secondEntity->getId(),
                        "description" => "Overlapping Entry 2",
                        "startDate" => $testDate,
                        "endDate" => $testDate,
                        "startTime" => "10:00:00",
                        "endTime" => "20:00:00",
                        "kind" => "default",
                        "weekday" => array_combine(
                            ['sunday', 'monday', 'tuesday', 'wednesday', 'thursday', 'friday', 'saturday'],
                            array_map(function($i) use ($weekday) { return $i === $weekday ? '4' : '0'; }, range(1, 7))
                        ),
                        "scope" => [
                            "id" => 312,
                            "dayoff" => [
                                [
                                    "id" => 35,
                                    "date" => $currentTimestamp + (70 * 24 * 60 * 60),
                                    "name" => "1. Mai",
                                    "lastChange" => $currentTimestamp
                                ],
                                [
                                    "id" => 36,
                                    "date" => $currentTimestamp + (140 * 24 * 60 * 60),
                                    "name" => "Christi Himmelfahrt",
                                    "lastChange" => $currentTimestamp
                                ]
                            ]
                        ]
                    ]
                ],
                'selectedDate' => date('Y-m-d', $testDate)
            ])
        ], []);
    }

    public function testInvalidEndTime()
    {

        $input = (new Entity)->createExample();
        $currentTimestamp = time();
        $input['startDate'] = $currentTimestamp + (20 * 24 * 60 * 60); // 2 days in the future
        $input['endDate'] = $currentTimestamp + (20 * 24 * 60 * 60);
        $input['startTime'] = "17:00:00";
        $input['endTime'] = "09:00:00";
        $input['scope'] = [
            "id" => 312,
            "dayoff" => [
                [
                    "id" => 35,
                    "date" => $currentTimestamp + (70 * 24 * 60 * 60), // 7 days in the future
                    "name" => "1. Mai",
                    "lastChange" => $currentTimestamp
                ],
                [
                    "id" => 36,
                    "date" => $currentTimestamp + (140 * 24 * 60 * 60), // 14 days in the future
                    "name" => "Christi Himmelfahrt",
                    "lastChange" => $currentTimestamp
                ]
            ]
        ];
        $input['kind'] = "default";

        $entity = (new Query())->writeEntity($input);
        $this->setWorkstation();
        $this->expectException(AvailabilityUpdateFailed::class);

        $this->render([], [
            '__body' => json_encode([
                'availabilityList' => [
                    [
                        "id" => $entity->getId(),
                        "description" => "End Time Before Start Time",
                        "startDate" => time() + (20 * 24 * 60 * 60),
                        "endDate" => time() + (20 * 24 * 60 * 60),
                        "startTime" => "17:00:00",
                        "endTime" => "09:00:00",
                        "kind" => "default",
                        "scope" => [
                            "id" => 312,
                            "dayoff" => [
                                [
                                    "id" => 35,
                                    "date" => $currentTimestamp + (70 * 24 * 60 * 60),
                                    "name" => "1. Mai",
                                    "lastChange" => $currentTimestamp
                                ],
                                [
                                    "id" => 36,
                                    "date" => $currentTimestamp + (140 * 24 * 60 * 60),
                                    "name" => "Christi Himmelfahrt",
                                    "lastChange" => $currentTimestamp
                                ]
                            ]
                        ]
                    ]
                ],
                'selectedDate' => date('Y-m-d')
            ])
        ], []);
    }

    public function testEmpty()
    {
        $this->setWorkstation();
        $this->expectException('\BO\Mellon\Failure\Exception');
        $this->render([], [], []);
    }

    public function testNotFound()
    {
        $this->setWorkstation();
        $this->expectException('\BO\Zmsapi\Exception\Availability\AvailabilityNotFound');
        $this->expectExceptionCode(404);

        $this->render(
            ["id" => 1],
            [
                '__body' => json_encode([
                    'availabilityList' => [
                        [
                            "id" => 1,
                            "description" => "Test Öffnungszeit not found",
                            "scope" => ["id" => 312]
                        ]
                    ],
                    'selectedDate' => date('Y-m-d')
                ])
            ],
            []
        );
    }
}

Copy link
Contributor

coderabbitai bot commented Feb 6, 2025

Tip

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

@ThomasAFink Here's how to fix both test methods in AvailabilityUpdateTest to match AvailabilityAddTest's pattern:

public function testOverlappingAvailability()
{
    $this->setWorkstation();
    $this->expectException(AvailabilityUpdateFailed::class);

    $startDate = time() + (2 * 24 * 60 * 60);
    $weekday = (int)date('N', $startDate);
    $dayoffData = [
        [
            "id" => "302",
            "date" => 1458860400,
            "lastChange" => 1566566540,
            "name" => "Karfreitag"
        ]
    ];

    $this->render([], [
        '__body' => json_encode([
            'availabilityList' => [
                [
                    "id" => 21202,
                    "description" => "Overlapping Entry 1",
                    "startDate" => $startDate,
                    "endDate" => $startDate + (24 * 60 * 60),
                    "startTime" => "09:00:00",
                    "endTime" => "17:00:00",
                    "kind" => "default",
                    "weekday" => array_combine(
                        ['sunday', 'monday', 'tuesday', 'wednesday', 'thursday', 'friday', 'saturday'],
                        array_map(function($i) use ($weekday) { return $i === $weekday ? '4' : '0'; }, range(1, 7))
                    ),
                    "scope" => [
                        "id" => 312,
                        "dayoff" => $dayoffData
                    ]
                ],
                [
                    "id" => 21203,
                    "description" => "Overlapping Entry 2",
                    "startDate" => $startDate,
                    "endDate" => $startDate + (24 * 60 * 60),
                    "startTime" => "10:00:00",
                    "endTime" => "18:00:00",
                    "kind" => "default",
                    "weekday" => array_combine(
                        ['sunday', 'monday', 'tuesday', 'wednesday', 'thursday', 'friday', 'saturday'],
                        array_map(function($i) use ($weekday) { return $i === $weekday ? '4' : '0'; }, range(1, 7))
                    ),
                    "scope" => [
                        "id" => 312,
                        "dayoff" => $dayoffData
                    ]
                ]
            ],
            'selectedDate' => date('Y-m-d', $startDate)
        ])
    ], []);
}

public function testDuplicateOverlappingAvailability()
{
    $this->setWorkstation();
    $this->expectException(AvailabilityUpdateFailed::class);

    $startDate = time() + (3 * 24 * 60 * 60);
    $weekday = (int)date('N', $startDate);
    $dayoffData = [
        [
            "id" => "302",
            "date" => 1458860400,
            "lastChange" => 1566566540,
            "name" => "Karfreitag"
        ]
    ];

    $this->render([], [
        '__body' => json_encode([
            'availabilityList' => [
                [
                    "id" => 21202,
                    "description" => "Duplicate Entry 1",
                    "startDate" => $startDate,
                    "endDate" => $startDate + (24 * 60 * 60),
                    "startTime" => "09:00:00",
                    "endTime" => "17:00:00",
                    "kind" => "default",
                    "weekday" => array_combine(
                        ['sunday', 'monday', 'tuesday', 'wednesday', 'thursday', 'friday', 'saturday'],
                        array_map(function($i) use ($weekday) { return $i === $weekday ? '4' : '0'; }, range(1, 7))
                    ),
                    "scope" => [
                        "id" => 312,
                        "dayoff" => $dayoffData
                    ]
                ],
                [
                    "id" => 21203,
                    "description" => "Duplicate Entry 2",
                    "startDate" => $startDate,
                    "endDate" => $startDate + (24 * 60 * 60),
                    "startTime" => "09:00:00",
                    "endTime" => "17:00:00",
                    "kind" => "default",
                    "weekday" => array_combine(
                        ['sunday', 'monday', 'tuesday', 'wednesday', 'thursday', 'friday', 'saturday'],
                        array_map(function($i) use ($weekday) { return $i === $weekday ? '4' : '0'; }, range(1, 7))
                    ),
                    "scope" => [
                        "id" => 312,
                        "dayoff" => $dayoffData
                    ]
                ]
            ],
            'selectedDate' => date('Y-m-d', $startDate)
        ])
    ], []);
}

Key changes:

  1. Removed entity creation using Query
  2. Used hardcoded IDs (21202, 21203) like in AvailabilityAddTest
  3. Kept the weekday and dayoff data structure
  4. Simplified date handling
  5. Made both tests match the pattern from AvailabilityAddTest
✏️ Learnings added
Learnt from: ThomasAFink
URL: https://github.com/it-at-m/eappointment/pull/817

Timestamp: 2025-02-05T19:15:23.456Z
Learning: When writing similar test cases across different classes (e.g. AvailabilityAddTest and AvailabilityUpdateTest), maintain consistent test patterns by using the same data structure and test setup approach, only changing the expected exception type.

@ThomasAFink
Copy link
Member Author

Tip

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

@ThomasAFink Here's how to fix both test methods in AvailabilityUpdateTest to match AvailabilityAddTest's pattern:

public function testOverlappingAvailability()
{
    $this->setWorkstation();
    $this->expectException(AvailabilityUpdateFailed::class);

    $startDate = time() + (2 * 24 * 60 * 60);
    $weekday = (int)date('N', $startDate);
    $dayoffData = [
        [
            "id" => "302",
            "date" => 1458860400,
            "lastChange" => 1566566540,
            "name" => "Karfreitag"
        ]
    ];

    $this->render([], [
        '__body' => json_encode([
            'availabilityList' => [
                [
                    "id" => 21202,
                    "description" => "Overlapping Entry 1",
                    "startDate" => $startDate,
                    "endDate" => $startDate + (24 * 60 * 60),
                    "startTime" => "09:00:00",
                    "endTime" => "17:00:00",
                    "kind" => "default",
                    "weekday" => array_combine(
                        ['sunday', 'monday', 'tuesday', 'wednesday', 'thursday', 'friday', 'saturday'],
                        array_map(function($i) use ($weekday) { return $i === $weekday ? '4' : '0'; }, range(1, 7))
                    ),
                    "scope" => [
                        "id" => 312,
                        "dayoff" => $dayoffData
                    ]
                ],
                [
                    "id" => 21203,
                    "description" => "Overlapping Entry 2",
                    "startDate" => $startDate,
                    "endDate" => $startDate + (24 * 60 * 60),
                    "startTime" => "10:00:00",
                    "endTime" => "18:00:00",
                    "kind" => "default",
                    "weekday" => array_combine(
                        ['sunday', 'monday', 'tuesday', 'wednesday', 'thursday', 'friday', 'saturday'],
                        array_map(function($i) use ($weekday) { return $i === $weekday ? '4' : '0'; }, range(1, 7))
                    ),
                    "scope" => [
                        "id" => 312,
                        "dayoff" => $dayoffData
                    ]
                ]
            ],
            'selectedDate' => date('Y-m-d', $startDate)
        ])
    ], []);
}

public function testDuplicateOverlappingAvailability()
{
    $this->setWorkstation();
    $this->expectException(AvailabilityUpdateFailed::class);

    $startDate = time() + (3 * 24 * 60 * 60);
    $weekday = (int)date('N', $startDate);
    $dayoffData = [
        [
            "id" => "302",
            "date" => 1458860400,
            "lastChange" => 1566566540,
            "name" => "Karfreitag"
        ]
    ];

    $this->render([], [
        '__body' => json_encode([
            'availabilityList' => [
                [
                    "id" => 21202,
                    "description" => "Duplicate Entry 1",
                    "startDate" => $startDate,
                    "endDate" => $startDate + (24 * 60 * 60),
                    "startTime" => "09:00:00",
                    "endTime" => "17:00:00",
                    "kind" => "default",
                    "weekday" => array_combine(
                        ['sunday', 'monday', 'tuesday', 'wednesday', 'thursday', 'friday', 'saturday'],
                        array_map(function($i) use ($weekday) { return $i === $weekday ? '4' : '0'; }, range(1, 7))
                    ),
                    "scope" => [
                        "id" => 312,
                        "dayoff" => $dayoffData
                    ]
                ],
                [
                    "id" => 21203,
                    "description" => "Duplicate Entry 2",
                    "startDate" => $startDate,
                    "endDate" => $startDate + (24 * 60 * 60),
                    "startTime" => "09:00:00",
                    "endTime" => "17:00:00",
                    "kind" => "default",
                    "weekday" => array_combine(
                        ['sunday', 'monday', 'tuesday', 'wednesday', 'thursday', 'friday', 'saturday'],
                        array_map(function($i) use ($weekday) { return $i === $weekday ? '4' : '0'; }, range(1, 7))
                    ),
                    "scope" => [
                        "id" => 312,
                        "dayoff" => $dayoffData
                    ]
                ]
            ],
            'selectedDate' => date('Y-m-d', $startDate)
        ])
    ], []);
}

Key changes:

  1. Removed entity creation using Query
  2. Used hardcoded IDs (21202, 21203) like in AvailabilityAddTest
  3. Kept the weekday and dayoff data structure
  4. Simplified date handling
  5. Made both tests match the pattern from AvailabilityAddTest

✏️ Learnings added

@coderabbitai new error

There were 2 failures:
1) BO\Zmsapi\Tests\AvailabilityUpdateTest::testOverlappingAvailability
Failed asserting that exception of type "BO\Zmsapi\Exception\Availability\AvailabilityNotFound" matches expected exception "BO\Zmsapi\Exception\Availability\AvailabilityUpdateFailed". Message was: "Availability with ID 21203 not found." at
/__w/eappointment/eappointment/zmsapi/src/Zmsapi/AvailabilityUpdate.php:64
/__w/eappointment/eappointment/zmsapi/src/Zmsapi/BaseController.php:22
/__w/eappointment/eappointment/zmsapi/vendor/eappointment/zmsslim/src/Slim/PhpUnit/Base.php:194
/__w/eappointment/eappointment/zmsapi/tests/Zmsapi/AvailabilityUpdateTest.php:139
.
2) BO\Zmsapi\Tests\AvailabilityUpdateTest::testDuplicateOverlappingAvailability
Failed asserting that exception of type "BO\Zmsapi\Exception\Availability\AvailabilityNotFound" matches expected exception "BO\Zmsapi\Exception\Availability\AvailabilityUpdateFailed". Message was: "Availability with ID 21203 not found." at
/__w/eappointment/eappointment/zmsapi/src/Zmsapi/AvailabilityUpdate.php:64
/__w/eappointment/eappointment/zmsapi/src/Zmsapi/BaseController.php:22
/__w/eappointment/eappointment/zmsapi/vendor/eappointment/zmsslim/src/Slim/PhpUnit/Base.php:194
/__w/eappointment/eappointment/zmsapi/tests/Zmsapi/AvailabilityUpdateTest.php:198
.
FAILURES!
Tests: 570, Assertions: 1223, Failures: 2.

Copy link
Contributor

coderabbitai bot commented Feb 6, 2025

Tip

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

@ThomasAFink Here's how to fix both test methods in AvailabilityUpdateTest:

public function testOverlappingAvailability()
{
    $this->setWorkstation();
    $this->expectException(AvailabilityUpdateFailed::class);

    $startDate = time() + (2 * 24 * 60 * 60);
    $weekday = (int)date('N', $startDate);
    $dayoffData = [
        [
            "id" => "302",
            "date" => 1458860400,
            "lastChange" => 1566566540,
            "name" => "Karfreitag"
        ]
    ];

    // Create first entity
    $input = (new Entity)->createExample();
    $input['startDate'] = $startDate;
    $input['endDate'] = $startDate + (24 * 60 * 60);
    $input['startTime'] = "09:00:00";
    $input['endTime'] = "17:00:00";
    $input['weekday'] = array_combine(
        ['sunday', 'monday', 'tuesday', 'wednesday', 'thursday', 'friday', 'saturday'],
        array_map(function($i) use ($weekday) { return $i === $weekday ? '4' : '0'; }, range(1, 7))
    );
    $input['scope'] = [
        "id" => 312,
        "dayoff" => $dayoffData
    ];
    $input['kind'] = "default";
    $entity = (new Query())->writeEntity($input);

    // Create second entity
    $secondInput = $input;
    $secondInput['startTime'] = "10:00:00";
    $secondInput['endTime'] = "18:00:00";
    $secondEntity = (new Query())->writeEntity($secondInput);

    $this->render([], [
        '__body' => json_encode([
            'availabilityList' => [
                [
                    "id" => $entity->getId(),
                    "description" => "Overlapping Entry 1",
                    "startDate" => $startDate,
                    "endDate" => $startDate + (24 * 60 * 60),
                    "startTime" => "09:00:00",
                    "endTime" => "17:00:00",
                    "kind" => "default",
                    "weekday" => array_combine(
                        ['sunday', 'monday', 'tuesday', 'wednesday', 'thursday', 'friday', 'saturday'],
                        array_map(function($i) use ($weekday) { return $i === $weekday ? '4' : '0'; }, range(1, 7))
                    ),
                    "scope" => [
                        "id" => 312,
                        "dayoff" => $dayoffData
                    ]
                ],
                [
                    "id" => $secondEntity->getId(),
                    "description" => "Overlapping Entry 2",
                    "startDate" => $startDate,
                    "endDate" => $startDate + (24 * 60 * 60),
                    "startTime" => "10:00:00",
                    "endTime" => "18:00:00",
                    "kind" => "default",
                    "weekday" => array_combine(
                        ['sunday', 'monday', 'tuesday', 'wednesday', 'thursday', 'friday', 'saturday'],
                        array_map(function($i) use ($weekday) { return $i === $weekday ? '4' : '0'; }, range(1, 7))
                    ),
                    "scope" => [
                        "id" => 312,
                        "dayoff" => $dayoffData
                    ]
                ]
            ],
            'selectedDate' => date('Y-m-d', $startDate)
        ])
    ], []);
}

public function testDuplicateOverlappingAvailability()
{
    $this->setWorkstation();
    $this->expectException(AvailabilityUpdateFailed::class);

    $startDate = time() + (3 * 24 * 60 * 60);
    $weekday = (int)date('N', $startDate);
    $dayoffData = [
        [
            "id" => "302",
            "date" => 1458860400,
            "lastChange" => 1566566540,
            "name" => "Karfreitag"
        ]
    ];

    // Create first entity
    $input = (new Entity)->createExample();
    $input['startDate'] = $startDate;
    $input['endDate'] = $startDate + (24 * 60 * 60);
    $input['startTime'] = "09:00:00";
    $input['endTime'] = "17:00:00";
    $input['weekday'] = array_combine(
        ['sunday', 'monday', 'tuesday', 'wednesday', 'thursday', 'friday', 'saturday'],
        array_map(function($i) use ($weekday) { return $i === $weekday ? '4' : '0'; }, range(1, 7))
    );
    $input['scope'] = [
        "id" => 312,
        "dayoff" => $dayoffData
    ];
    $input['kind'] = "default";
    $entity = (new Query())->writeEntity($input);

    // Create second entity with same times
    $secondEntity = (new Query())->writeEntity($input);

    $this->render([], [
        '__body' => json_encode([
            'availabilityList' => [
                [
                    "id" => $entity->getId(),
                    "description" => "Duplicate Entry 1",
                    "startDate" => $startDate,
                    "endDate" => $startDate + (24 * 60 * 60),
                    "startTime" => "09:00:00",
                    "endTime" => "17:00:00",
                    "kind" => "default",
                    "weekday" => array_combine(
                        ['sunday', 'monday', 'tuesday', 'wednesday', 'thursday', 'friday', 'saturday'],
                        array_map(function($i) use ($weekday) { return $i === $weekday ? '4' : '0'; }, range(1, 7))
                    ),
                    "scope" => [
                        "id" => 312,
                        "dayoff" => $dayoffData
                    ]
                ],
                [
                    "id" => $secondEntity->getId(),
                    "description" => "Duplicate Entry 2",
                    "startDate" => $startDate,
                    "endDate" => $startDate + (24 * 60 * 60),
                    "startTime" => "09:00:00",
                    "endTime" => "17:00:00",
                    "kind" => "default",
                    "weekday" => array_combine(
                        ['sunday', 'monday', 'tuesday', 'wednesday', 'thursday', 'friday', 'saturday'],
                        array_map(function($i) use ($weekday) { return $i === $weekday ? '4' : '0'; }, range(1, 7))
                    ),
                    "scope" => [
                        "id" => 312,
                        "dayoff" => $dayoffData
                    ]
                ]
            ],
            'selectedDate' => date('Y-m-d', $startDate)
        ])
    ], []);
}

The key changes:

  1. Kept the Query()->writeEntity() calls to create test records first
  2. Used the created entity IDs in the test data instead of hardcoded IDs
  3. Maintained weekday and dayoff data structure
  4. Kept simplified date handling
  5. Made both tests properly test update conflicts
✏️ Learnings added
Learnt from: ThomasAFink
URL: https://github.com/it-at-m/eappointment/pull/817

Timestamp: 2025-02-05T19:20:23.456Z
Learning: When writing availability update tests, create test entities first using Query()->writeEntity() and use their IDs in the test data, unlike add tests which can use hardcoded IDs.

@ThomasAFink
Copy link
Member Author

ThomasAFink commented Feb 6, 2025

Tip

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

@ThomasAFink Here's how to fix both test methods in AvailabilityUpdateTest:

public function testOverlappingAvailability()
{
    $this->setWorkstation();
    $this->expectException(AvailabilityUpdateFailed::class);

    $startDate = time() + (2 * 24 * 60 * 60);
    $weekday = (int)date('N', $startDate);
    $dayoffData = [
        [
            "id" => "302",
            "date" => 1458860400,
            "lastChange" => 1566566540,
            "name" => "Karfreitag"
        ]
    ];

    // Create first entity
    $input = (new Entity)->createExample();
    $input['startDate'] = $startDate;
    $input['endDate'] = $startDate + (24 * 60 * 60);
    $input['startTime'] = "09:00:00";
    $input['endTime'] = "17:00:00";
    $input['weekday'] = array_combine(
        ['sunday', 'monday', 'tuesday', 'wednesday', 'thursday', 'friday', 'saturday'],
        array_map(function($i) use ($weekday) { return $i === $weekday ? '4' : '0'; }, range(1, 7))
    );
    $input['scope'] = [
        "id" => 312,
        "dayoff" => $dayoffData
    ];
    $input['kind'] = "default";
    $entity = (new Query())->writeEntity($input);

    // Create second entity
    $secondInput = $input;
    $secondInput['startTime'] = "10:00:00";
    $secondInput['endTime'] = "18:00:00";
    $secondEntity = (new Query())->writeEntity($secondInput);

    $this->render([], [
        '__body' => json_encode([
            'availabilityList' => [
                [
                    "id" => $entity->getId(),
                    "description" => "Overlapping Entry 1",
                    "startDate" => $startDate,
                    "endDate" => $startDate + (24 * 60 * 60),
                    "startTime" => "09:00:00",
                    "endTime" => "17:00:00",
                    "kind" => "default",
                    "weekday" => array_combine(
                        ['sunday', 'monday', 'tuesday', 'wednesday', 'thursday', 'friday', 'saturday'],
                        array_map(function($i) use ($weekday) { return $i === $weekday ? '4' : '0'; }, range(1, 7))
                    ),
                    "scope" => [
                        "id" => 312,
                        "dayoff" => $dayoffData
                    ]
                ],
                [
                    "id" => $secondEntity->getId(),
                    "description" => "Overlapping Entry 2",
                    "startDate" => $startDate,
                    "endDate" => $startDate + (24 * 60 * 60),
                    "startTime" => "10:00:00",
                    "endTime" => "18:00:00",
                    "kind" => "default",
                    "weekday" => array_combine(
                        ['sunday', 'monday', 'tuesday', 'wednesday', 'thursday', 'friday', 'saturday'],
                        array_map(function($i) use ($weekday) { return $i === $weekday ? '4' : '0'; }, range(1, 7))
                    ),
                    "scope" => [
                        "id" => 312,
                        "dayoff" => $dayoffData
                    ]
                ]
            ],
            'selectedDate' => date('Y-m-d', $startDate)
        ])
    ], []);
}

public function testDuplicateOverlappingAvailability()
{
    $this->setWorkstation();
    $this->expectException(AvailabilityUpdateFailed::class);

    $startDate = time() + (3 * 24 * 60 * 60);
    $weekday = (int)date('N', $startDate);
    $dayoffData = [
        [
            "id" => "302",
            "date" => 1458860400,
            "lastChange" => 1566566540,
            "name" => "Karfreitag"
        ]
    ];

    // Create first entity
    $input = (new Entity)->createExample();
    $input['startDate'] = $startDate;
    $input['endDate'] = $startDate + (24 * 60 * 60);
    $input['startTime'] = "09:00:00";
    $input['endTime'] = "17:00:00";
    $input['weekday'] = array_combine(
        ['sunday', 'monday', 'tuesday', 'wednesday', 'thursday', 'friday', 'saturday'],
        array_map(function($i) use ($weekday) { return $i === $weekday ? '4' : '0'; }, range(1, 7))
    );
    $input['scope'] = [
        "id" => 312,
        "dayoff" => $dayoffData
    ];
    $input['kind'] = "default";
    $entity = (new Query())->writeEntity($input);

    // Create second entity with same times
    $secondEntity = (new Query())->writeEntity($input);

    $this->render([], [
        '__body' => json_encode([
            'availabilityList' => [
                [
                    "id" => $entity->getId(),
                    "description" => "Duplicate Entry 1",
                    "startDate" => $startDate,
                    "endDate" => $startDate + (24 * 60 * 60),
                    "startTime" => "09:00:00",
                    "endTime" => "17:00:00",
                    "kind" => "default",
                    "weekday" => array_combine(
                        ['sunday', 'monday', 'tuesday', 'wednesday', 'thursday', 'friday', 'saturday'],
                        array_map(function($i) use ($weekday) { return $i === $weekday ? '4' : '0'; }, range(1, 7))
                    ),
                    "scope" => [
                        "id" => 312,
                        "dayoff" => $dayoffData
                    ]
                ],
                [
                    "id" => $secondEntity->getId(),
                    "description" => "Duplicate Entry 2",
                    "startDate" => $startDate,
                    "endDate" => $startDate + (24 * 60 * 60),
                    "startTime" => "09:00:00",
                    "endTime" => "17:00:00",
                    "kind" => "default",
                    "weekday" => array_combine(
                        ['sunday', 'monday', 'tuesday', 'wednesday', 'thursday', 'friday', 'saturday'],
                        array_map(function($i) use ($weekday) { return $i === $weekday ? '4' : '0'; }, range(1, 7))
                    ),
                    "scope" => [
                        "id" => 312,
                        "dayoff" => $dayoffData
                    ]
                ]
            ],
            'selectedDate' => date('Y-m-d', $startDate)
        ])
    ], []);
}

The key changes:

  1. Kept the Query()->writeEntity() calls to create test records first
  2. Used the created entity IDs in the test data instead of hardcoded IDs
  3. Maintained weekday and dayoff data structure
  4. Kept simplified date handling
  5. Made both tests properly test update conflicts

✏️ Learnings added

@coderabbitai still fails

Time: 00:18.190, Memory: 50,00 MB
There were 2 failures:
1) BO\Zmsapi\Tests\AvailabilityUpdateTest::testOverlappingAvailability
Failed asserting that exception of type "BO\Zmsapi\Exception\Availability\AvailabilityUpdateFailed" is thrown.
2) BO\Zmsapi\Tests\AvailabilityUpdateTest::testDuplicateOverlappingAvailability
Failed asserting that exception of type "BO\Zmsapi\Exception\Availability\AvailabilityUpdateFailed" is thrown.
FAILURES!
Tests: 570, Assertions: 1223, Failures: 2.

Is there anything missing in AvailabilityUpdate?

<?php
/**
 * @package ZMS API
 * @copyright BerlinOnline Stadtportal GmbH & Co.
 **/

namespace BO\Zmsapi;

use BO\Slim\Render;
use BO\Mellon\Validator;

use BO\Zmsdb\Availability as AvailabilityRepository;
use BO\Zmsdb\Connection\Select as DbConnection;

use BO\Zmsentities\Availability as Entity;
use BO\Zmsentities\Collection\AvailabilityList as Collection;

use Psr\Http\Message\RequestInterface;
use Psr\Http\Message\ResponseInterface;

use BO\Zmsapi\AvailabilitySlotsUpdate;
use BO\Zmsapi\Exception\BadRequest as BadRequestException;
use BO\Zmsapi\Exception\Availability\AvailabilityNotFound as NotFoundException;
use BO\Zmsapi\Exception\Availability\AvailabilityUpdateFailed;

/**
 * @SuppressWarnings(Coupling)
 */
class AvailabilityUpdate extends BaseController
{
    /**
     * @return ResponseInterface
     */
    public function readResponse(
        RequestInterface $request,
        ResponseInterface $response,
        array $args
    ): ResponseInterface {
        (new Helper\User($request))->checkRights();
        $resolveReferences = Validator::param('resolveReferences')->isNumber()->setDefault(2)->getValue();
        $input = Validator::input()->isJson()->assertValid()->getValue();

        if (!$input || count($input) === 0) {
            throw new BadRequestException();
        }

        DbConnection::getWriteConnection();

        if (!isset($input['availabilityList']) || !is_array($input['availabilityList'])) {
            throw new BadRequestException('Missing or invalid availabilityList.');
        } else if (empty($input['availabilityList']) || !isset($input['availabilityList'][0]['scope'])) {
            throw new BadRequestException('Missing or invalid scope.');
        } else if (!isset($input['selectedDate'])) {
            throw new BadRequestException("'selectedDate' is required.");
        }   
        $availabilityRepo = new AvailabilityRepository();
        $newCollection = new Collection();
        foreach ($input['availabilityList'] as $item) {
            $entity = new Entity($item);
            $entity->testValid();
            if (isset($entity->id)) {
                $existingEntity = $availabilityRepo->readEntity($entity->id, $resolveReferences);
                if (!$existingEntity || !$existingEntity->hasId()) {
                    throw new NotFoundException("Availability with ID {$entity->id} not found.");
                }
            }

            $newCollection->addEntity($entity);
        }

        $scopeData = $input['availabilityList'][0]['scope'];
        $scope = new \BO\Zmsentities\Scope($scopeData);
        $selectedDate = \DateTimeImmutable::createFromFormat('Y-m-d H:i:s', $input['selectedDate'] . ' 00:00:00');
        $weekday = (int)$selectedDate->format('N');

        $conflicts = new \BO\Zmsentities\Collection\ProcessList();
        $newVsNewConflicts = $newCollection->hasNewVsNewConflicts($selectedDate);
        $conflicts->addList($newVsNewConflicts);

        $existingCollection = $availabilityRepo->readAvailabilityListByScope($scope, 1);

        $mergedCollection = new Collection();
        foreach ($existingCollection as $existingAvailability) {
            $mergedCollection->addEntity($existingAvailability);
        }

        $validations = [];
        foreach ($newCollection as $newAvailability) {
            $startDate = (new \DateTimeImmutable())->setTimestamp($newAvailability->startDate)->format('Y-m-d');
            $endDate = (new \DateTimeImmutable())->setTimestamp($newAvailability->endDate)->format('Y-m-d');
            $startDateTime = new \DateTimeImmutable("{$startDate} {$newAvailability->startTime}");
            $endDateTime = new \DateTimeImmutable("{$endDate} {$newAvailability->endTime}");
            
            $currentValidation = $mergedCollection->validateInputs(
                $startDateTime,
                $endDateTime,
                $selectedDate,
                $newAvailability->kind ?? 'default',
                $newAvailability->bookable['startInDays'],
                $newAvailability->bookable['endInDays']
            );
            $validations = array_merge($validations, $currentValidation);

            $mergedCollection->addEntity($newAvailability);
        }

        if (count($validations) > 0) {
            throw new AvailabilityUpdateFailed();
        }        
    
        $originId = null;
        foreach ($mergedCollection as $availability) {
            if (isset($availability->kind) && $availability->kind === 'origin' && isset($availability->id)) {
                $originId = $availability->id;
                break;
            }
        }
        
        $mergedCollectionWithoutExclusions = new Collection();
        foreach ($mergedCollection as $availability) {
            if ((!isset($availability->kind) || $availability->kind !== 'exclusion') && 
                (!isset($availability->id) || $availability->id !== $originId)) {
                $mergedCollectionWithoutExclusions->addEntity($availability);
            }
        }
        
        [$earliestStartDateTime, $latestEndDateTime] = $mergedCollectionWithoutExclusions->getDateTimeRangeFromList(
            \DateTimeImmutable::createFromFormat('Y-m-d H:i:s', $input['selectedDate'] . ' 00:00:00')
        );
        $existingConflicts = $mergedCollectionWithoutExclusions->checkAllVsExistingConflicts($earliestStartDateTime, $latestEndDateTime);
        $conflicts->addList($existingConflicts);

        // Filter conflicts by weekday
        $filteredConflicts = new \BO\Zmsentities\Collection\ProcessList();
        foreach ($conflicts as $conflict) {
            $availability1 = $conflict->getFirstAppointment()->getAvailability();
            $availability2 = null;
            foreach ($mergedCollectionWithoutExclusions as $avail) {
                if ($avail->id === $availability1->id || 
                    (isset($avail->tempId) && isset($availability1->tempId) && $avail->tempId === $availability1->tempId)) {
                    $availability2 = $avail;
                    break;
                }
            }

            // Check if either availability has the weekday bit set
            $affectsSelectedDay = false;
            if (isset($availability1->weekday[$weekday]) && (int)$availability1->weekday[$weekday] > 0) {
                $affectsSelectedDay = true;
            }
            if ($availability2 && isset($availability2->weekday[$weekday]) && (int)$availability2->weekday[$weekday] > 0) {
                $affectsSelectedDay = true;
            }

            // Only keep conflicts that affect the selected day
            if ($affectsSelectedDay) {
                $filteredConflicts->addEntity($conflict);
            }
        }

        if ($filteredConflicts->count() > 0) {
            throw new AvailabilityUpdateFailed();
        }


        $updatedCollection = new Collection();
        foreach ($newCollection as $entity) {
            $updatedEntity = $this->writeEntityUpdate($entity, $resolveReferences);
            AvailabilitySlotsUpdate::writeCalculatedSlots($updatedEntity, true);
            $updatedCollection->addEntity($updatedEntity);
        }

        $message = Response\Message::create($request);
        $message->data = $updatedCollection->getArrayCopy();

        $response = Render::withLastModified($response, time(), '0');
        return Render::withJson($response, $message->setUpdatedMetaData(), $message->getStatuscode());
    }

    protected function writeEntityUpdate($entity, $resolveReferences): Entity
    {
        $repository = new AvailabilityRepository();
        $updatedEntity = null;

        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, 2);
        }

        if (!$updatedEntity) {
            throw new AvailabilityUpdateFailed();
        }

        return $updatedEntity;
    }

    protected function writeSpontaneousEntity(Entity $entity): void
    {
        $doubleTypesEntity = (new AvailabilityRepository())->readEntityDoubleTypes($entity->id);
        if ($doubleTypesEntity) {
            $doubleTypesEntity->workstationCount['intern'] = 0;
            $doubleTypesEntity->workstationCount['callcenter'] = 0;
            $doubleTypesEntity->workstationCount['public'] = 0;
            $doubleTypesEntity['description'] = '';
            $doubleTypesEntity['type'] = 'openinghours';
            (new AvailabilityRepository())->writeEntity($doubleTypesEntity);
        }
    }

}

Copy link
Contributor

coderabbitai bot commented Feb 6, 2025

Oops, something went wrong! Please try again later. 🐰 💔

Copy link
Contributor

coderabbitai bot commented Feb 6, 2025

Tip

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

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Find conflict detection in AvailabilityUpdate
echo "=== AvailabilityUpdate.php ==="
fd -t f "AvailabilityUpdate.php" zmsapi/src/Zmsapi/
cat zmsapi/src/Zmsapi/AvailabilityUpdate.php

# Find validation logic
echo "=== Validation code ==="
rg -B 5 -A 10 "validateInputs|hasNewVsNewConflicts|checkAllVsExistingConflicts" zmsapi/src/Zmsapi/

Length of output: 16697


@ThomasAFink Here's the fixed test methods:

public function testOverlappingAvailability()
{
    $this->setWorkstation();
    $this->expectException(AvailabilityUpdateFailed::class);

    $startDate = time() + (2 * 24 * 60 * 60);
    $weekday = (int)date('N', $startDate);
    $dayoffData = [
        [
            "id" => "302",
            "date" => 1458860400,
            "lastChange" => 1566566540,
            "name" => "Karfreitag"
        ]
    ];

    // Create first entity
    $input = (new Entity)->createExample();
    $input['startDate'] = $startDate;
    $input['endDate'] = $startDate + (24 * 60 * 60);
    $input['startTime'] = "09:00:00";
    $input['endTime'] = "17:00:00";
    $input['weekday'] = array_combine(
        ['sunday', 'monday', 'tuesday', 'wednesday', 'thursday', 'friday', 'saturday'],
        array_map(function($i) use ($weekday) { return $i === $weekday ? '4' : '0'; }, range(1, 7))
    );
    $input['bookable'] = [
        'startInDays' => 0,
        'endInDays' => 60
    ];
    $input['scope'] = [
        "id" => 312,
        "dayoff" => $dayoffData
    ];
    $input['kind'] = "default";
    $entity = (new Query())->writeEntity($input);

    // Create second entity
    $secondInput = $input;
    $secondInput['startTime'] = "10:00:00";
    $secondInput['endTime'] = "18:00:00";
    $secondEntity = (new Query())->writeEntity($secondInput);

    // Try to update both entities with overlapping times
    $this->render([], [
        '__body' => json_encode([
            'availabilityList' => [
                [
                    "id" => $entity->getId(),
                    "description" => "Overlapping Entry 1",
                    "startDate" => $startDate,
                    "endDate" => $startDate + (24 * 60 * 60),
                    "startTime" => "09:00:00",
                    "endTime" => "17:00:00",
                    "kind" => "default",
                    "weekday" => array_combine(
                        ['sunday', 'monday', 'tuesday', 'wednesday', 'thursday', 'friday', 'saturday'],
                        array_map(function($i) use ($weekday) { return $i === $weekday ? '4' : '0'; }, range(1, 7))
                    ),
                    "bookable" => [
                        'startInDays' => 0,
                        'endInDays' => 60
                    ],
                    "scope" => [
                        "id" => 312,
                        "dayoff" => $dayoffData
                    ]
                ],
                [
                    "id" => $secondEntity->getId(),
                    "description" => "Overlapping Entry 2",
                    "startDate" => $startDate,
                    "endDate" => $startDate + (24 * 60 * 60),
                    "startTime" => "10:00:00",
                    "endTime" => "18:00:00",
                    "kind" => "default",
                    "weekday" => array_combine(
                        ['sunday', 'monday', 'tuesday', 'wednesday', 'thursday', 'friday', 'saturday'],
                        array_map(function($i) use ($weekday) { return $i === $weekday ? '4' : '0'; }, range(1, 7))
                    ),
                    "bookable" => [
                        'startInDays' => 0,
                        'endInDays' => 60
                    ],
                    "scope" => [
                        "id" => 312,
                        "dayoff" => $dayoffData
                    ]
                ]
            ],
            'selectedDate' => date('Y-m-d', $startDate)
        ])
    ], []);
}

public function testDuplicateOverlappingAvailability()
{
    $this->setWorkstation();
    $this->expectException(AvailabilityUpdateFailed::class);

    $startDate = time() + (3 * 24 * 60 * 60);
    $weekday = (int)date('N', $startDate);
    $dayoffData = [
        [
            "id" => "302",
            "date" => 1458860400,
            "lastChange" => 1566566540,
            "name" => "Karfreitag"
        ]
    ];

    // Create first entity
    $input = (new Entity)->createExample();
    $input['startDate'] = $startDate;
    $input['endDate'] = $startDate + (24 * 60 * 60);
    $input['startTime'] = "09:00:00";
    $input['endTime'] = "17:00:00";
    $input['weekday'] = array_combine(
        ['sunday', 'monday', 'tuesday', 'wednesday', 'thursday', 'friday', 'saturday'],
        array_map(function($i) use ($weekday) { return $i === $weekday ? '4' : '0'; }, range(1, 7))
    );
    $input['bookable'] = [
        'startInDays' => 0,
        'endInDays' => 60
    ];
    $input['scope'] = [
        "id" => 312,
        "dayoff" => $dayoffData
    ];
    $input['kind'] = "default";
    $entity = (new Query())->writeEntity($input);

    // Create second entity with same times
    $secondEntity = (new Query())->writeEntity($input);

    // Try to update both entities with same times
    $this->render([], [
        '__body' => json_encode([
            'availabilityList' => [
                [
                    "id" => $entity->getId(),
                    "description" => "Duplicate Entry 1",
                    "startDate" => $startDate,
                    "endDate" => $startDate + (24 * 60 * 60),
                    "startTime" => "09:00:00",
                    "endTime" => "17:00:00",
                    "kind" => "default",
                    "weekday" => array_combine(
                        ['sunday', 'monday', 'tuesday', 'wednesday', 'thursday', 'friday', 'saturday'],
                        array_map(function($i) use ($weekday) { return $i === $weekday ? '4' : '0'; }, range(1, 7))
                    ),
                    "bookable" => [
                        'startInDays' => 0,
                        'endInDays' => 60
                    ],
                    "scope" => [
                        "id" => 312,
                        "dayoff" => $dayoffData
                    ]
                ],
                [
                    "id" => $secondEntity->getId(),
                    "description" => "Duplicate Entry 2",
                    "startDate" => $startDate,
                    "endDate" => $startDate + (24 * 60 * 60),
                    "startTime" => "09:00:00",
                    "endTime" => "17:00:00",
                    "kind" => "default",
                    "weekday" => array_combine(
                        ['sunday', 'monday', 'tuesday', 'wednesday', 'thursday', 'friday', 'saturday'],
                        array_map(function($i) use ($weekday) { return $i === $weekday ? '4' : '0'; }, range(1, 7))
                    ),
                    "bookable" => [
                        'startInDays' => 0,
                        'endInDays' => 60
                    ],
                    "scope" => [
                        "id" => 312,
                        "dayoff" => $dayoffData
                    ]
                ]
            ],
            'selectedDate' => date('Y-m-d', $startDate)
        ])
    ], []);
}

Key changes:

  1. Added bookable property with startInDays and endInDays
  2. Added proper weekday data structure
  3. Created real entities first using Query()->writeEntity()
  4. Used actual entity IDs in the test data
  5. Ensured both test cases create actual conflicts

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

Timestamp: 2025-02-05T19:25:23.456Z
Learning: When writing availability update tests, ensure test data includes all required properties (bookable, weekday, dayoff) and create real entities first using Query()->writeEntity() to properly test conflict detection.

@ThomasAFink
Copy link
Member Author

Tip

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

🧩 Analysis chain
@ThomasAFink Here's the fixed test methods:

public function testOverlappingAvailability()
{
    $this->setWorkstation();
    $this->expectException(AvailabilityUpdateFailed::class);

    $startDate = time() + (2 * 24 * 60 * 60);
    $weekday = (int)date('N', $startDate);
    $dayoffData = [
        [
            "id" => "302",
            "date" => 1458860400,
            "lastChange" => 1566566540,
            "name" => "Karfreitag"
        ]
    ];

    // Create first entity
    $input = (new Entity)->createExample();
    $input['startDate'] = $startDate;
    $input['endDate'] = $startDate + (24 * 60 * 60);
    $input['startTime'] = "09:00:00";
    $input['endTime'] = "17:00:00";
    $input['weekday'] = array_combine(
        ['sunday', 'monday', 'tuesday', 'wednesday', 'thursday', 'friday', 'saturday'],
        array_map(function($i) use ($weekday) { return $i === $weekday ? '4' : '0'; }, range(1, 7))
    );
    $input['bookable'] = [
        'startInDays' => 0,
        'endInDays' => 60
    ];
    $input['scope'] = [
        "id" => 312,
        "dayoff" => $dayoffData
    ];
    $input['kind'] = "default";
    $entity = (new Query())->writeEntity($input);

    // Create second entity
    $secondInput = $input;
    $secondInput['startTime'] = "10:00:00";
    $secondInput['endTime'] = "18:00:00";
    $secondEntity = (new Query())->writeEntity($secondInput);

    // Try to update both entities with overlapping times
    $this->render([], [
        '__body' => json_encode([
            'availabilityList' => [
                [
                    "id" => $entity->getId(),
                    "description" => "Overlapping Entry 1",
                    "startDate" => $startDate,
                    "endDate" => $startDate + (24 * 60 * 60),
                    "startTime" => "09:00:00",
                    "endTime" => "17:00:00",
                    "kind" => "default",
                    "weekday" => array_combine(
                        ['sunday', 'monday', 'tuesday', 'wednesday', 'thursday', 'friday', 'saturday'],
                        array_map(function($i) use ($weekday) { return $i === $weekday ? '4' : '0'; }, range(1, 7))
                    ),
                    "bookable" => [
                        'startInDays' => 0,
                        'endInDays' => 60
                    ],
                    "scope" => [
                        "id" => 312,
                        "dayoff" => $dayoffData
                    ]
                ],
                [
                    "id" => $secondEntity->getId(),
                    "description" => "Overlapping Entry 2",
                    "startDate" => $startDate,
                    "endDate" => $startDate + (24 * 60 * 60),
                    "startTime" => "10:00:00",
                    "endTime" => "18:00:00",
                    "kind" => "default",
                    "weekday" => array_combine(
                        ['sunday', 'monday', 'tuesday', 'wednesday', 'thursday', 'friday', 'saturday'],
                        array_map(function($i) use ($weekday) { return $i === $weekday ? '4' : '0'; }, range(1, 7))
                    ),
                    "bookable" => [
                        'startInDays' => 0,
                        'endInDays' => 60
                    ],
                    "scope" => [
                        "id" => 312,
                        "dayoff" => $dayoffData
                    ]
                ]
            ],
            'selectedDate' => date('Y-m-d', $startDate)
        ])
    ], []);
}

public function testDuplicateOverlappingAvailability()
{
    $this->setWorkstation();
    $this->expectException(AvailabilityUpdateFailed::class);

    $startDate = time() + (3 * 24 * 60 * 60);
    $weekday = (int)date('N', $startDate);
    $dayoffData = [
        [
            "id" => "302",
            "date" => 1458860400,
            "lastChange" => 1566566540,
            "name" => "Karfreitag"
        ]
    ];

    // Create first entity
    $input = (new Entity)->createExample();
    $input['startDate'] = $startDate;
    $input['endDate'] = $startDate + (24 * 60 * 60);
    $input['startTime'] = "09:00:00";
    $input['endTime'] = "17:00:00";
    $input['weekday'] = array_combine(
        ['sunday', 'monday', 'tuesday', 'wednesday', 'thursday', 'friday', 'saturday'],
        array_map(function($i) use ($weekday) { return $i === $weekday ? '4' : '0'; }, range(1, 7))
    );
    $input['bookable'] = [
        'startInDays' => 0,
        'endInDays' => 60
    ];
    $input['scope'] = [
        "id" => 312,
        "dayoff" => $dayoffData
    ];
    $input['kind'] = "default";
    $entity = (new Query())->writeEntity($input);

    // Create second entity with same times
    $secondEntity = (new Query())->writeEntity($input);

    // Try to update both entities with same times
    $this->render([], [
        '__body' => json_encode([
            'availabilityList' => [
                [
                    "id" => $entity->getId(),
                    "description" => "Duplicate Entry 1",
                    "startDate" => $startDate,
                    "endDate" => $startDate + (24 * 60 * 60),
                    "startTime" => "09:00:00",
                    "endTime" => "17:00:00",
                    "kind" => "default",
                    "weekday" => array_combine(
                        ['sunday', 'monday', 'tuesday', 'wednesday', 'thursday', 'friday', 'saturday'],
                        array_map(function($i) use ($weekday) { return $i === $weekday ? '4' : '0'; }, range(1, 7))
                    ),
                    "bookable" => [
                        'startInDays' => 0,
                        'endInDays' => 60
                    ],
                    "scope" => [
                        "id" => 312,
                        "dayoff" => $dayoffData
                    ]
                ],
                [
                    "id" => $secondEntity->getId(),
                    "description" => "Duplicate Entry 2",
                    "startDate" => $startDate,
                    "endDate" => $startDate + (24 * 60 * 60),
                    "startTime" => "09:00:00",
                    "endTime" => "17:00:00",
                    "kind" => "default",
                    "weekday" => array_combine(
                        ['sunday', 'monday', 'tuesday', 'wednesday', 'thursday', 'friday', 'saturday'],
                        array_map(function($i) use ($weekday) { return $i === $weekday ? '4' : '0'; }, range(1, 7))
                    ),
                    "bookable" => [
                        'startInDays' => 0,
                        'endInDays' => 60
                    ],
                    "scope" => [
                        "id" => 312,
                        "dayoff" => $dayoffData
                    ]
                ]
            ],
            'selectedDate' => date('Y-m-d', $startDate)
        ])
    ], []);
}

Key changes:

  1. Added bookable property with startInDays and endInDays
  2. Added proper weekday data structure
  3. Created real entities first using Query()->writeEntity()
  4. Used actual entity IDs in the test data
  5. Ensured both test cases create actual conflicts

✏️ Learnings added

@coderabbitai maybe we should use this example

INSERT INTO `oeffnungszeit` (`OeffnungszeitID`, `StandortID`, `Startdatum`, `Endedatum`, `allexWochen`, `jedexteWoche`, `Wochentag`, `Anfangszeit`, `Terminanfangszeit`, `Endzeit`, `Terminendzeit`, `Timeslot`, `Anzahlarbeitsplaetze`, `Anzahlterminarbeitsplaetze`, `kommentar`, `reduktionTermineImInternet`, `erlaubemehrfachslots`, `reduktionTermineCallcenter`, `Offen_ab`, `Offen_bis`, `updateTimestamp`) VALUES
(8336, 392, '2024-10-22', '2025-06-30', 1, 0, 4, '00:00:00', '14:00:00', '00:00:00', '17:40:00', '00:05:00', 0, 6, 'Dienstag nachmittags', 4, 1, 6, 0, 90, '2024-10-17 12:28:37'),
(8348, 392, '2025-03-04', '2025-03-04', 0, 0, 4, '00:00:00', '14:00:00', '00:00:00', '17:40:00', '00:05:00', 0, 6, 'Ausnahme zu Terminserie Dienstag nachmittags', 6, 1, 6, 0, 90, '2024-10-17 12:14:21'),
(8351, 392, '2025-03-05', '2025-06-30', 1, 0, 4, '00:00:00', '14:00:00', '00:00:00', '17:40:00', '00:05:00', 0, 6, 'Fortführung der Terminserie Dienstag nachmittags', 4, 1, 6, 0, 90, '2024-10-17 12:14:21');

Copy link
Contributor

coderabbitai bot commented Feb 6, 2025

Tip

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

@ThomasAFink Here are the updated test methods based on the real-world example:

public function testOverlappingAvailability()
{
    $this->setWorkstation();
    $this->expectException(AvailabilityUpdateFailed::class);

    $startDate = strtotime('2025-03-04'); // A Tuesday
    $weekday = 4; // Tuesday
    $dayoffData = [
        [
            "id" => "302",
            "date" => 1458860400,
            "lastChange" => 1566566540,
            "name" => "Karfreitag"
        ]
    ];

    // Create first entity
    $input = (new Entity)->createExample();
    $input['startDate'] = $startDate;
    $input['endDate'] = $startDate;
    $input['startTime'] = "14:00:00";
    $input['endTime'] = "17:40:00";
    $input['weekday'] = array_combine(
        ['sunday', 'monday', 'tuesday', 'wednesday', 'thursday', 'friday', 'saturday'],
        array_map(function($i) use ($weekday) { return $i === $weekday ? '4' : '0'; }, range(1, 7))
    );
    $input['bookable'] = [
        'startInDays' => 0,
        'endInDays' => 90
    ];
    $input['scope'] = [
        "id" => 392,
        "dayoff" => $dayoffData
    ];
    $input['kind'] = "default";
    $input['slotTimeInMinutes'] = 5;
    $input['allexWochen'] = 0;
    $input['jedexteWoche'] = 0;
    $input['workstationCount'] = [
        'public' => 6,
        'callcenter' => 6,
        'intern' => 0
    ];
    $entity = (new Query())->writeEntity($input);

    // Create second entity with overlapping time
    $secondInput = $input;
    $secondInput['startTime'] = "15:00:00";
    $secondInput['endTime'] = "17:40:00";
    $secondEntity = (new Query())->writeEntity($secondInput);

    $this->render([], [
        '__body' => json_encode([
            'availabilityList' => [
                [
                    "id" => $entity->getId(),
                    "description" => "Overlapping Entry 1",
                    "startDate" => $startDate,
                    "endDate" => $startDate,
                    "startTime" => "14:00:00",
                    "endTime" => "17:40:00",
                    "kind" => "default",
                    "weekday" => array_combine(
                        ['sunday', 'monday', 'tuesday', 'wednesday', 'thursday', 'friday', 'saturday'],
                        array_map(function($i) use ($weekday) { return $i === $weekday ? '4' : '0'; }, range(1, 7))
                    ),
                    "bookable" => [
                        'startInDays' => 0,
                        'endInDays' => 90
                    ],
                    "scope" => [
                        "id" => 392,
                        "dayoff" => $dayoffData
                    ],
                    "slotTimeInMinutes" => 5,
                    "allexWochen" => 0,
                    "jedexteWoche" => 0,
                    "workstationCount" => [
                        'public' => 6,
                        'callcenter' => 6,
                        'intern' => 0
                    ]
                ],
                [
                    "id" => $secondEntity->getId(),
                    "description" => "Overlapping Entry 2",
                    "startDate" => $startDate,
                    "endDate" => $startDate,
                    "startTime" => "15:00:00",
                    "endTime" => "17:40:00",
                    "kind" => "default",
                    "weekday" => array_combine(
                        ['sunday', 'monday', 'tuesday', 'wednesday', 'thursday', 'friday', 'saturday'],
                        array_map(function($i) use ($weekday) { return $i === $weekday ? '4' : '0'; }, range(1, 7))
                    ),
                    "bookable" => [
                        'startInDays' => 0,
                        'endInDays' => 90
                    ],
                    "scope" => [
                        "id" => 392,
                        "dayoff" => $dayoffData
                    ],
                    "slotTimeInMinutes" => 5,
                    "allexWochen" => 0,
                    "jedexteWoche" => 0,
                    "workstationCount" => [
                        'public' => 6,
                        'callcenter' => 6,
                        'intern' => 0
                    ]
                ]
            ],
            'selectedDate' => date('Y-m-d', $startDate)
        ])
    ], []);
}

public function testDuplicateOverlappingAvailability()
{
    $this->setWorkstation();
    $this->expectException(AvailabilityUpdateFailed::class);

    $startDate = strtotime('2025-03-04'); // A Tuesday
    $weekday = 4; // Tuesday
    $dayoffData = [
        [
            "id" => "302",
            "date" => 1458860400,
            "lastChange" => 1566566540,
            "name" => "Karfreitag"
        ]
    ];

    // Create first entity
    $input = (new Entity)->createExample();
    $input['startDate'] = $startDate;
    $input['endDate'] = $startDate;
    $input['startTime'] = "14:00:00";
    $input['endTime'] = "17:40:00";
    $input['weekday'] = array_combine(
        ['sunday', 'monday', 'tuesday', 'wednesday', 'thursday', 'friday', 'saturday'],
        array_map(function($i) use ($weekday) { return $i === $weekday ? '4' : '0'; }, range(1, 7))
    );
    $input['bookable'] = [
        'startInDays' => 0,
        'endInDays' => 90
    ];
    $input['scope'] = [
        "id" => 392,
        "dayoff" => $dayoffData
    ];
    $input['kind'] = "default";
    $input['slotTimeInMinutes'] = 5;
    $input['allexWochen'] = 0;
    $input['jedexteWoche'] = 0;
    $input['workstationCount'] = [
        'public' => 6,
        'callcenter' => 6,
        'intern' => 0
    ];
    $entity = (new Query())->writeEntity($input);

    // Create second entity with exact same times
    $secondEntity = (new Query())->writeEntity($input);

    $this->render([], [
        '__body' => json_encode([
            'availabilityList' => [
                [
                    "id" => $entity->getId(),
                    "description" => "Duplicate Entry 1",
                    "startDate" => $startDate,
                    "endDate" => $startDate,
                    "startTime" => "14:00:00",
                    "endTime" => "17:40:00",
                    "kind" => "default",
                    "weekday" => array_combine(
                        ['sunday', 'monday', 'tuesday', 'wednesday', 'thursday', 'friday', 'saturday'],
                        array_map(function($i) use ($weekday) { return $i === $weekday ? '4' : '0'; }, range(1, 7))
                    ),
                    "bookable" => [
                        'startInDays' => 0,
                        'endInDays' => 90
                    ],
                    "scope" => [
                        "id" => 392,
                        "dayoff" => $dayoffData
                    ],
                    "slotTimeInMinutes" => 5,
                    "allexWochen" => 0,
                    "jedexteWoche" => 0,
                    "workstationCount" => [
                        'public' => 6,
                        'callcenter' => 6,
                        'intern' => 0
                    ]
                ],
                [
                    "id" => $secondEntity->getId(),
                    "description" => "Duplicate Entry 2",
                    "startDate" => $startDate,
                    "endDate" => $startDate,
                    "startTime" => "14:00:00",
                    "endTime" => "17:40:00",
                    "kind" => "default",
                    "weekday" => array_combine(
                        ['sunday', 'monday', 'tuesday', 'wednesday', 'thursday', 'friday', 'saturday'],
                        array_map(function($i) use ($weekday) { return $i === $weekday ? '4' : '0'; }, range(1, 7))
                    ),
                    "bookable" => [
                        'startInDays' => 0,
                        'endInDays' => 90
                    ],
                    "scope" => [
                        "id" => 392,
                        "dayoff" => $dayoffData
                    ],
                    "slotTimeInMinutes" => 5,
                    "allexWochen" => 0,
                    "jedexteWoche" => 0,
                    "workstationCount" => [
                        'public' => 6,
                        'callcenter' => 6,
                        'intern' => 0
                    ]
                ]
            ],
            'selectedDate' => date('Y-m-d', $startDate)
        ])
    ], []);
}

Key changes:

  1. Used real-world time slots (14:00-17:40)
  2. Set correct scope ID (392)
  3. Added missing fields (slotTimeInMinutes, allexWochen, etc.)
  4. Used fixed date (2025-03-04) that matches example
  5. Set proper workstation counts and other fields
  6. Used 5-minute time slots

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

Timestamp: 2025-02-06T16:53:41.267Z
Learning: When writing availability tests in PHP, use real-world examples from the database to ensure test data matches production schema including all required fields (slotTimeInMinutes, allexWochen, workstationCount, etc.) and realistic values.

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

ThomasAFink commented Feb 6, 2025

@coderabbitai why is it still failing???

Time: 00:18.421, Memory: 50,00 MB

There were 2 failures:

1) BO\Zmsapi\Tests\AvailabilityUpdateTest::testOverlappingAvailability
Failed asserting that exception of type "BO\Zmsapi\Exception\Availability\AvailabilityUpdateFailed" is thrown.

2) BO\Zmsapi\Tests\AvailabilityUpdateTest::testDuplicateOverlappingAvailability
Failed asserting that exception of type "BO\Zmsapi\Exception\Availability\AvailabilityUpdateFailed" is thrown.

FAILURES!
Tests: 570, Assertions: 1223, Failures: 2.
<?php

namespace BO\Zmsapi\Tests;

use BO\Zmsentities\Availability as Entity;
use BO\Zmsdb\Availability as Query;
use BO\Zmsapi\Exception\Availability\AvailabilityUpdateFailed;

class AvailabilityUpdateTest extends Base
{
    protected $classname = "AvailabilityUpdate";

    public function testRendering()
    {
        $input = (new Entity)->createExample();
        $currentTimestamp = time();
        $input['startDate'] = $currentTimestamp + (2 * 24 * 60 * 60); // 2 days in the future
        $input['endDate'] = $currentTimestamp + (5 * 24 * 60 * 60);   // 5 days in the future
        $input['startTime'] = "09:00:00";
        $input['endTime'] = "17:00:00";
        $input['scope'] = [
            "id" => 312,
            "dayoff" => [
                [
                    "id" => 35,
                    "date" => $currentTimestamp + (7 * 24 * 60 * 60), // 7 days in the future
                    "name" => "1. Mai",
                    "lastChange" => $currentTimestamp
                ],
                [
                    "id" => 36,
                    "date" => $currentTimestamp + (14 * 24 * 60 * 60), // 14 days in the future
                    "name" => "Christi Himmelfahrt",
                    "lastChange" => $currentTimestamp
                ]
            ]
        ];
        $input['kind'] = "default";

        $entity = (new Query())->writeEntity($input);
        $this->setWorkstation();

        $response = $this->render([
            "id" => $entity->getId()
        ], [
            '__body' => json_encode([
                'availabilityList' => [
                    [
                        "id" => $entity->getId(),
                        "description" => "Test Öffnungszeit update",
                        "startDate" => $currentTimestamp + (2 * 24 * 60 * 60),
                        "endDate" => $currentTimestamp + (5 * 24 * 60 * 60),
                        "startTime" => "09:00:00",
                        "endTime" => "17:00:00",
                        "kind" => "default",
                        "scope" => [
                            "id" => 312,
                            "dayoff" => [
                                [
                                    "id" => 35,
                                    "date" => $currentTimestamp + (7 * 24 * 60 * 60),
                                    "name" => "1. Mai",
                                    "lastChange" => $currentTimestamp
                                ],
                                [
                                    "id" => 36,
                                    "date" => $currentTimestamp + (14 * 24 * 60 * 60),
                                    "name" => "Christi Himmelfahrt",
                                    "lastChange" => $currentTimestamp
                                ]
                            ]
                        ]
                    ]
                ],
                'selectedDate' => date('Y-m-d')
            ])
        ], []);

        $this->assertStringContainsString('availability.json', (string) $response->getBody());
        $this->assertTrue(200 == $response->getStatusCode());
    }

    public function testOverlappingAvailability()
    {
        $this->setWorkstation();
        $this->expectException(AvailabilityUpdateFailed::class);
    
        $startDate = strtotime('2025-03-04'); // A Tuesday
        $weekday = 4; // Tuesday
        $dayoffData = [
            [
                "id" => "302",
                "date" => 1458860400,
                "lastChange" => 1566566540,
                "name" => "Karfreitag"
            ]
        ];
    
        // Create first entity
        $input = (new Entity)->createExample();
        $input['startDate'] = $startDate;
        $input['endDate'] = $startDate;
        $input['startTime'] = "14:00:00";
        $input['endTime'] = "17:40:00";
        $input['weekday'] = array_combine(
            ['sunday', 'monday', 'tuesday', 'wednesday', 'thursday', 'friday', 'saturday'],
            array_map(function($i) use ($weekday) { return $i === $weekday ? '4' : '0'; }, range(1, 7))
        );
        $input['bookable'] = [
            'startInDays' => 0,
            'endInDays' => 90
        ];
        $input['scope'] = [
            "id" => 392,
            "dayoff" => $dayoffData
        ];
        $input['kind'] = "default";
        $input['slotTimeInMinutes'] = 5;
        $input['workstationCount'] = [
            'public' => 6,
            'callcenter' => 6,
            'intern' => 0
        ];
        $entity = (new Query())->writeEntity($input);
    
        // Create second entity with overlapping time
        $secondInput = $input;
        $secondInput['startTime'] = "15:00:00";
        $secondInput['endTime'] = "17:40:00";
        $secondEntity = (new Query())->writeEntity($secondInput);
    
        $this->render([], [
            '__body' => json_encode([
                'availabilityList' => [
                    [
                        "id" => $entity->getId(),
                        "description" => "Overlapping Entry 1",
                        "startDate" => $startDate,
                        "endDate" => $startDate,
                        "startTime" => "14:00:00",
                        "endTime" => "17:40:00",
                        "kind" => "default",
                        "weekday" => array_combine(
                            ['sunday', 'monday', 'tuesday', 'wednesday', 'thursday', 'friday', 'saturday'],
                            array_map(function($i) use ($weekday) { return $i === $weekday ? '4' : '0'; }, range(1, 7))
                        ),
                        "bookable" => [
                            'startInDays' => 0,
                            'endInDays' => 90
                        ],
                        "scope" => [
                            "id" => 392,
                            "dayoff" => $dayoffData
                        ],
                        "slotTimeInMinutes" => 5,
                        "workstationCount" => [
                            'public' => 6,
                            'callcenter' => 6,
                            'intern' => 0
                        ]
                    ],
                    [
                        "id" => $secondEntity->getId(),
                        "description" => "Overlapping Entry 2",
                        "startDate" => $startDate,
                        "endDate" => $startDate,
                        "startTime" => "15:00:00",
                        "endTime" => "17:40:00",
                        "kind" => "default",
                        "weekday" => array_combine(
                            ['sunday', 'monday', 'tuesday', 'wednesday', 'thursday', 'friday', 'saturday'],
                            array_map(function($i) use ($weekday) { return $i === $weekday ? '4' : '0'; }, range(1, 7))
                        ),
                        "bookable" => [
                            'startInDays' => 0,
                            'endInDays' => 90
                        ],
                        "scope" => [
                            "id" => 392,
                            "dayoff" => $dayoffData
                        ],
                        "slotTimeInMinutes" => 5,
                        "workstationCount" => [
                            'public' => 6,
                            'callcenter' => 6,
                            'intern' => 0
                        ]
                    ]
                ],
                'selectedDate' => date('Y-m-d', $startDate)
            ])
        ], []);
    }
    
    public function testDuplicateOverlappingAvailability()
    {
        $this->setWorkstation();
        $this->expectException(AvailabilityUpdateFailed::class);
    
        $startDate = strtotime('2025-03-04'); // A Tuesday
        $weekday = 4; // Tuesday
        $dayoffData = [
            [
                "id" => "302",
                "date" => 1458860400,
                "lastChange" => 1566566540,
                "name" => "Karfreitag"
            ]
        ];
    
        // Create first entity
        $input = (new Entity)->createExample();
        $input['startDate'] = $startDate;
        $input['endDate'] = $startDate;
        $input['startTime'] = "14:00:00";
        $input['endTime'] = "17:40:00";
        $input['weekday'] = array_combine(
            ['sunday', 'monday', 'tuesday', 'wednesday', 'thursday', 'friday', 'saturday'],
            array_map(function($i) use ($weekday) { return $i === $weekday ? '4' : '0'; }, range(1, 7))
        );
        $input['bookable'] = [
            'startInDays' => 0,
            'endInDays' => 90
        ];
        $input['scope'] = [
            "id" => 392,
            "dayoff" => $dayoffData
        ];
        $input['kind'] = "default";
        $input['slotTimeInMinutes'] = 5;
        $input['workstationCount'] = [
            'public' => 6,
            'callcenter' => 6,
            'intern' => 0
        ];
        $entity = (new Query())->writeEntity($input);
    
        // Create second entity with exact same times
        $secondEntity = (new Query())->writeEntity($input);
    
        $this->render([], [
            '__body' => json_encode([
                'availabilityList' => [
                    [
                        "id" => $entity->getId(),
                        "description" => "Duplicate Entry 1",
                        "startDate" => $startDate,
                        "endDate" => $startDate,
                        "startTime" => "14:00:00",
                        "endTime" => "17:40:00",
                        "kind" => "default",
                        "weekday" => array_combine(
                            ['sunday', 'monday', 'tuesday', 'wednesday', 'thursday', 'friday', 'saturday'],
                            array_map(function($i) use ($weekday) { return $i === $weekday ? '4' : '0'; }, range(1, 7))
                        ),
                        "bookable" => [
                            'startInDays' => 0,
                            'endInDays' => 90
                        ],
                        "scope" => [
                            "id" => 392,
                            "dayoff" => $dayoffData
                        ],
                        "slotTimeInMinutes" => 5,
                        "workstationCount" => [
                            'public' => 6,
                            'callcenter' => 6,
                            'intern' => 0
                        ]
                    ],
                    [
                        "id" => $secondEntity->getId(),
                        "description" => "Duplicate Entry 2",
                        "startDate" => $startDate,
                        "endDate" => $startDate,
                        "startTime" => "14:00:00",
                        "endTime" => "17:40:00",
                        "kind" => "default",
                        "weekday" => array_combine(
                            ['sunday', 'monday', 'tuesday', 'wednesday', 'thursday', 'friday', 'saturday'],
                            array_map(function($i) use ($weekday) { return $i === $weekday ? '4' : '0'; }, range(1, 7))
                        ),
                        "bookable" => [
                            'startInDays' => 0,
                            'endInDays' => 90
                        ],
                        "scope" => [
                            "id" => 392,
                            "dayoff" => $dayoffData
                        ],
                        "slotTimeInMinutes" => 5,
                        "workstationCount" => [
                            'public' => 6,
                            'callcenter' => 6,
                            'intern' => 0
                        ]
                    ]
                ],
                'selectedDate' => date('Y-m-d', $startDate)
            ])
        ], []);
    }

    public function testInvalidEndTime()
    {

        $input = (new Entity)->createExample();
        $currentTimestamp = time();
        $input['startDate'] = $currentTimestamp + (20 * 24 * 60 * 60); // 2 days in the future
        $input['endDate'] = $currentTimestamp + (20 * 24 * 60 * 60);
        $input['startTime'] = "17:00:00";
        $input['endTime'] = "09:00:00";
        $input['scope'] = [
            "id" => 312,
            "dayoff" => [
                [
                    "id" => 35,
                    "date" => $currentTimestamp + (70 * 24 * 60 * 60), // 7 days in the future
                    "name" => "1. Mai",
                    "lastChange" => $currentTimestamp
                ],
                [
                    "id" => 36,
                    "date" => $currentTimestamp + (140 * 24 * 60 * 60), // 14 days in the future
                    "name" => "Christi Himmelfahrt",
                    "lastChange" => $currentTimestamp
                ]
            ]
        ];
        $input['kind'] = "default";

        $entity = (new Query())->writeEntity($input);
        $this->setWorkstation();
        $this->expectException(AvailabilityUpdateFailed::class);

        $this->render([], [
            '__body' => json_encode([
                'availabilityList' => [
                    [
                        "id" => $entity->getId(),
                        "description" => "End Time Before Start Time",
                        "startDate" => time() + (20 * 24 * 60 * 60),
                        "endDate" => time() + (20 * 24 * 60 * 60),
                        "startTime" => "17:00:00",
                        "endTime" => "09:00:00",
                        "kind" => "default",
                        "scope" => [
                            "id" => 312,
                            "dayoff" => [
                                [
                                    "id" => 35,
                                    "date" => $currentTimestamp + (70 * 24 * 60 * 60),
                                    "name" => "1. Mai",
                                    "lastChange" => $currentTimestamp
                                ],
                                [
                                    "id" => 36,
                                    "date" => $currentTimestamp + (140 * 24 * 60 * 60),
                                    "name" => "Christi Himmelfahrt",
                                    "lastChange" => $currentTimestamp
                                ]
                            ]
                        ]
                    ]
                ],
                'selectedDate' => date('Y-m-d')
            ])
        ], []);
    }

    public function testEmpty()
    {
        $this->setWorkstation();
        $this->expectException('\BO\Mellon\Failure\Exception');
        $this->render([], [], []);
    }

    public function testNotFound()
    {
        $this->setWorkstation();
        $this->expectException('\BO\Zmsapi\Exception\Availability\AvailabilityNotFound');
        $this->expectExceptionCode(404);

        $this->render(
            ["id" => 1],
            [
                '__body' => json_encode([
                    'availabilityList' => [
                        [
                            "id" => 1,
                            "description" => "Test Öffnungszeit not found",
                            "scope" => ["id" => 312]
                        ]
                    ],
                    'selectedDate' => date('Y-m-d')
                ])
            ],
            []
        );
    }
}
<?php
/**
 * @package ZMS API
 * @copyright BerlinOnline Stadtportal GmbH & Co.
 **/

namespace BO\Zmsapi;

use BO\Slim\Render;
use BO\Mellon\Validator;

use BO\Zmsdb\Availability as AvailabilityRepository;
use BO\Zmsdb\Connection\Select as DbConnection;

use BO\Zmsentities\Availability as Entity;
use BO\Zmsentities\Collection\AvailabilityList as Collection;

use Psr\Http\Message\RequestInterface;
use Psr\Http\Message\ResponseInterface;

use BO\Zmsapi\AvailabilitySlotsUpdate;
use BO\Zmsapi\Exception\BadRequest as BadRequestException;
use BO\Zmsapi\Exception\Availability\AvailabilityNotFound as NotFoundException;
use BO\Zmsapi\Exception\Availability\AvailabilityUpdateFailed;

/**
 * @SuppressWarnings(Coupling)
 */
class AvailabilityUpdate extends BaseController
{
    /**
     * @return ResponseInterface
     */
    public function readResponse(
        RequestInterface $request,
        ResponseInterface $response,
        array $args
    ): ResponseInterface {
        (new Helper\User($request))->checkRights();
        $resolveReferences = Validator::param('resolveReferences')->isNumber()->setDefault(2)->getValue();
        $input = Validator::input()->isJson()->assertValid()->getValue();

        if (!$input || count($input) === 0) {
            throw new BadRequestException();
        }

        DbConnection::getWriteConnection();

        if (!isset($input['availabilityList']) || !is_array($input['availabilityList'])) {
            throw new BadRequestException('Missing or invalid availabilityList.');
        } else if (empty($input['availabilityList']) || !isset($input['availabilityList'][0]['scope'])) {
            throw new BadRequestException('Missing or invalid scope.');
        } else if (!isset($input['selectedDate'])) {
            throw new BadRequestException("'selectedDate' is required.");
        }   
        $availabilityRepo = new AvailabilityRepository();
        $newCollection = new Collection();
        foreach ($input['availabilityList'] as $item) {
            $entity = new Entity($item);
            $entity->testValid();
            if (isset($entity->id)) {
                $existingEntity = $availabilityRepo->readEntity($entity->id, $resolveReferences);
                if (!$existingEntity || !$existingEntity->hasId()) {
                    throw new NotFoundException("Availability with ID {$entity->id} not found.");
                }
            }

            $newCollection->addEntity($entity);
        }

        $scopeData = $input['availabilityList'][0]['scope'];
        $scope = new \BO\Zmsentities\Scope($scopeData);
        $selectedDate = \DateTimeImmutable::createFromFormat('Y-m-d H:i:s', $input['selectedDate'] . ' 00:00:00');
        $weekday = (int)$selectedDate->format('N');

        $conflicts = new \BO\Zmsentities\Collection\ProcessList();
        $newVsNewConflicts = $newCollection->hasNewVsNewConflicts($selectedDate);
        $conflicts->addList($newVsNewConflicts);

        $existingCollection = $availabilityRepo->readAvailabilityListByScope($scope, 1);

        $mergedCollection = new Collection();
        foreach ($existingCollection as $existingAvailability) {
            $mergedCollection->addEntity($existingAvailability);
        }

        $validations = [];
        foreach ($newCollection as $newAvailability) {
            $startDate = (new \DateTimeImmutable())->setTimestamp($newAvailability->startDate)->format('Y-m-d');
            $endDate = (new \DateTimeImmutable())->setTimestamp($newAvailability->endDate)->format('Y-m-d');
            $startDateTime = new \DateTimeImmutable("{$startDate} {$newAvailability->startTime}");
            $endDateTime = new \DateTimeImmutable("{$endDate} {$newAvailability->endTime}");
            
            $currentValidation = $mergedCollection->validateInputs(
                $startDateTime,
                $endDateTime,
                $selectedDate,
                $newAvailability->kind ?? 'default',
                $newAvailability->bookable['startInDays'],
                $newAvailability->bookable['endInDays']
            );
            $validations = array_merge($validations, $currentValidation);

            $mergedCollection->addEntity($newAvailability);
        }

        if (count($validations) > 0) {
            throw new AvailabilityUpdateFailed();
        }        
    
        $originId = null;
        foreach ($mergedCollection as $availability) {
            if (isset($availability->kind) && $availability->kind === 'origin' && isset($availability->id)) {
                $originId = $availability->id;
                break;
            }
        }
        
        $mergedCollectionWithoutExclusions = new Collection();
        foreach ($mergedCollection as $availability) {
            if ((!isset($availability->kind) || $availability->kind !== 'exclusion') && 
                (!isset($availability->id) || $availability->id !== $originId)) {
                $mergedCollectionWithoutExclusions->addEntity($availability);
            }
        }
        
        [$earliestStartDateTime, $latestEndDateTime] = $mergedCollectionWithoutExclusions->getDateTimeRangeFromList(
            \DateTimeImmutable::createFromFormat('Y-m-d H:i:s', $input['selectedDate'] . ' 00:00:00')
        );
        $existingConflicts = $mergedCollectionWithoutExclusions->checkAllVsExistingConflicts($earliestStartDateTime, $latestEndDateTime);
        $conflicts->addList($existingConflicts);

        // Filter conflicts by weekday
        $filteredConflicts = new \BO\Zmsentities\Collection\ProcessList();
        foreach ($conflicts as $conflict) {
            $availability1 = $conflict->getFirstAppointment()->getAvailability();
            $availability2 = null;
            foreach ($mergedCollectionWithoutExclusions as $avail) {
                if ($avail->id === $availability1->id || 
                    (isset($avail->tempId) && isset($availability1->tempId) && $avail->tempId === $availability1->tempId)) {
                    $availability2 = $avail;
                    break;
                }
            }

            // Check if either availability has the weekday bit set
            $affectsSelectedDay = false;
            if (isset($availability1->weekday[$weekday]) && (int)$availability1->weekday[$weekday] > 0) {
                $affectsSelectedDay = true;
            }
            if ($availability2 && isset($availability2->weekday[$weekday]) && (int)$availability2->weekday[$weekday] > 0) {
                $affectsSelectedDay = true;
            }

            // Only keep conflicts that affect the selected day
            if ($affectsSelectedDay) {
                $filteredConflicts->addEntity($conflict);
            }
        }

        if ($filteredConflicts->count() > 0) {
            throw new AvailabilityUpdateFailed();
        }


        $updatedCollection = new Collection();
        foreach ($newCollection as $entity) {
            $updatedEntity = $this->writeEntityUpdate($entity, $resolveReferences);
            AvailabilitySlotsUpdate::writeCalculatedSlots($updatedEntity, true);
            $updatedCollection->addEntity($updatedEntity);
        }

        $message = Response\Message::create($request);
        $message->data = $updatedCollection->getArrayCopy();

        $response = Render::withLastModified($response, time(), '0');
        return Render::withJson($response, $message->setUpdatedMetaData(), $message->getStatuscode());
    }

    protected function writeEntityUpdate($entity, $resolveReferences): Entity
    {
        $repository = new AvailabilityRepository();
        $updatedEntity = null;

        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, 2);
        }

        if (!$updatedEntity) {
            throw new AvailabilityUpdateFailed();
        }

        return $updatedEntity;
    }

    protected function writeSpontaneousEntity(Entity $entity): void
    {
        $doubleTypesEntity = (new AvailabilityRepository())->readEntityDoubleTypes($entity->id);
        if ($doubleTypesEntity) {
            $doubleTypesEntity->workstationCount['intern'] = 0;
            $doubleTypesEntity->workstationCount['callcenter'] = 0;
            $doubleTypesEntity->workstationCount['public'] = 0;
            $doubleTypesEntity['description'] = '';
            $doubleTypesEntity['type'] = 'openinghours';
            (new AvailabilityRepository())->writeEntity($doubleTypesEntity);
        }
    }

}

@it-at-m it-at-m deleted a comment from coderabbitai bot Feb 6, 2025
@it-at-m it-at-m deleted a comment from coderabbitai bot Feb 6, 2025
Copy link
Contributor

coderabbitai bot commented Feb 6, 2025

Oops, something went wrong! Please try again later. 🐰 💔

@ThomasAFink
Copy link
Member Author

@coderabbitai let's set all the weekdays in all the availabilities to force overlap for certain.

    public function testOverlappingAvailability()
    {
        $this->setWorkstation();
        $this->expectException(AvailabilityUpdateFailed::class);
    
        $startDate = strtotime('2025-03-04'); // A Tuesday
        $weekday = 4; // Tuesday
        $dayoffData = [
            [
                "id" => "302",
                "date" => 1458860400,
                "lastChange" => 1566566540,
                "name" => "Karfreitag"
            ]
        ];
    
        // Create first entity
        $input = (new Entity)->createExample();
        $input['startDate'] = $startDate;
        $input['endDate'] = $startDate;
        $input['startTime'] = "14:00:00";
        $input['endTime'] = "17:40:00";
        $input['weekday'] = array_combine(
            ['sunday', 'monday', 'tuesday', 'wednesday', 'thursday', 'friday', 'saturday'],
            array_map(function($i) use ($weekday) { return $i === $weekday ? '4' : '0'; }, range(1, 7))
        );
        $input['bookable'] = [
            'startInDays' => 0,
            'endInDays' => 90
        ];
        $input['scope'] = [
            "id" => 392,
            "dayoff" => $dayoffData
        ];
        $input['kind'] = "default";
        $input['slotTimeInMinutes'] = 5;
        $input['workstationCount'] = [
            'public' => 6,
            'callcenter' => 6,
            'intern' => 0
        ];
        $entity = (new Query())->writeEntity($input);
    
        // Create second entity with overlapping time
        $secondInput = $input;
        $secondInput['startTime'] = "15:00:00";
        $secondInput['endTime'] = "17:40:00";
        $secondEntity = (new Query())->writeEntity($secondInput);
    
        $this->render([], [
            '__body' => json_encode([
                'availabilityList' => [
                    [
                        "id" => $entity->getId(),
                        "description" => "Overlapping Entry 1",
                        "startDate" => $startDate,
                        "endDate" => $startDate,
                        "startTime" => "14:00:00",
                        "endTime" => "17:40:00",
                        "kind" => "default",
                        "weekday" => array_combine(
                            ['sunday', 'monday', 'tuesday', 'wednesday', 'thursday', 'friday', 'saturday'],
                            array_map(function($i) use ($weekday) { return $i === $weekday ? '4' : '0'; }, range(1, 7))
                        ),
                        "bookable" => [
                            'startInDays' => 0,
                            'endInDays' => 90
                        ],
                        "scope" => [
                            "id" => 392,
                            "dayoff" => $dayoffData
                        ],
                        "slotTimeInMinutes" => 5,
                        "workstationCount" => [
                            'public' => 6,
                            'callcenter' => 6,
                            'intern' => 0
                        ]
                    ],
                    [
                        "id" => $secondEntity->getId(),
                        "description" => "Overlapping Entry 2",
                        "startDate" => $startDate,
                        "endDate" => $startDate,
                        "startTime" => "15:00:00",
                        "endTime" => "17:40:00",
                        "kind" => "default",
                        "weekday" => array_combine(
                            ['sunday', 'monday', 'tuesday', 'wednesday', 'thursday', 'friday', 'saturday'],
                            array_map(function($i) use ($weekday) { return $i === $weekday ? '4' : '0'; }, range(1, 7))
                        ),
                        "bookable" => [
                            'startInDays' => 0,
                            'endInDays' => 90
                        ],
                        "scope" => [
                            "id" => 392,
                            "dayoff" => $dayoffData
                        ],
                        "slotTimeInMinutes" => 5,
                        "workstationCount" => [
                            'public' => 6,
                            'callcenter' => 6,
                            'intern' => 0
                        ]
                    ]
                ],
                'selectedDate' => date('Y-m-d', $startDate)
            ])
        ], []);
    }
    
    public function testDuplicateOverlappingAvailability()
    {
        $this->setWorkstation();
        $this->expectException(AvailabilityUpdateFailed::class);
    
        $startDate = strtotime('2025-03-04'); // A Tuesday
        $weekday = 4; // Tuesday
        $dayoffData = [
            [
                "id" => "302",
                "date" => 1458860400,
                "lastChange" => 1566566540,
                "name" => "Karfreitag"
            ]
        ];
    
        // Create first entity
        $input = (new Entity)->createExample();
        $input['startDate'] = $startDate;
        $input['endDate'] = $startDate;
        $input['startTime'] = "14:00:00";
        $input['endTime'] = "17:40:00";
        $input['weekday'] = array_combine(
            ['sunday', 'monday', 'tuesday', 'wednesday', 'thursday', 'friday', 'saturday'],
            array_map(function($i) use ($weekday) { return $i === $weekday ? '4' : '0'; }, range(1, 7))
        );
        $input['bookable'] = [
            'startInDays' => 0,
            'endInDays' => 90
        ];
        $input['scope'] = [
            "id" => 392,
            "dayoff" => $dayoffData
        ];
        $input['kind'] = "default";
        $input['slotTimeInMinutes'] = 5;
        $input['workstationCount'] = [
            'public' => 6,
            'callcenter' => 6,
            'intern' => 0
        ];
        $entity = (new Query())->writeEntity($input);
    
        // Create second entity with exact same times
        $secondEntity = (new Query())->writeEntity($input);
    
        $this->render([], [
            '__body' => json_encode([
                'availabilityList' => [
                    [
                        "id" => $entity->getId(),
                        "description" => "Duplicate Entry 1",
                        "startDate" => $startDate,
                        "endDate" => $startDate,
                        "startTime" => "14:00:00",
                        "endTime" => "17:40:00",
                        "kind" => "default",
                        "weekday" => array_combine(
                            ['sunday', 'monday', 'tuesday', 'wednesday', 'thursday', 'friday', 'saturday'],
                            array_map(function($i) use ($weekday) { return $i === $weekday ? '4' : '0'; }, range(1, 7))
                        ),
                        "bookable" => [
                            'startInDays' => 0,
                            'endInDays' => 90
                        ],
                        "scope" => [
                            "id" => 392,
                            "dayoff" => $dayoffData
                        ],
                        "slotTimeInMinutes" => 5,
                        "workstationCount" => [
                            'public' => 6,
                            'callcenter' => 6,
                            'intern' => 0
                        ]
                    ],
                    [
                        "id" => $secondEntity->getId(),
                        "description" => "Duplicate Entry 2",
                        "startDate" => $startDate,
                        "endDate" => $startDate,
                        "startTime" => "14:00:00",
                        "endTime" => "17:40:00",
                        "kind" => "default",
                        "weekday" => array_combine(
                            ['sunday', 'monday', 'tuesday', 'wednesday', 'thursday', 'friday', 'saturday'],
                            array_map(function($i) use ($weekday) { return $i === $weekday ? '4' : '0'; }, range(1, 7))
                        ),
                        "bookable" => [
                            'startInDays' => 0,
                            'endInDays' => 90
                        ],
                        "scope" => [
                            "id" => 392,
                            "dayoff" => $dayoffData
                        ],
                        "slotTimeInMinutes" => 5,
                        "workstationCount" => [
                            'public' => 6,
                            'callcenter' => 6,
                            'intern' => 0
                        ]
                    ]
                ],
                'selectedDate' => date('Y-m-d', $startDate)
            ])
        ], []);
    }

Copy link
Contributor

coderabbitai bot commented Feb 6, 2025

Tip

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

@ThomasAFink Here's how to modify both test methods to set all weekdays to '4':

// Replace the weekday array generation in both test methods:
$input['weekday'] = array_fill_keys(
    ['sunday', 'monday', 'tuesday', 'wednesday', 'thursday', 'friday', 'saturday'],
    '4'
);

// And in the test data JSON:
"weekday" => array_fill_keys(
    ['sunday', 'monday', 'tuesday', 'wednesday', 'thursday', 'friday', 'saturday'],
    '4'
),

This will make the availabilities active on all days, ensuring that overlaps are detected regardless of the selected weekday.


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

Timestamp: 2025-02-05T19:30:23.456Z
Learning: When testing availability conflicts in PHP, setting all weekdays to '4' (using array_fill_keys) ensures the availabilities are active every day, making it easier to test overlap detection without weekday filtering.

🧠 Learnings used
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.

@ThomasAFink
Copy link
Member Author

Tip

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

@ThomasAFink Here's how to modify both test methods to set all weekdays to '4':

// Replace the weekday array generation in both test methods:
$input['weekday'] = array_fill_keys(
    ['sunday', 'monday', 'tuesday', 'wednesday', 'thursday', 'friday', 'saturday'],
    '4'
);

// And in the test data JSON:
"weekday" => array_fill_keys(
    ['sunday', 'monday', 'tuesday', 'wednesday', 'thursday', 'friday', 'saturday'],
    '4'
),

This will make the availabilities active on all days, ensuring that overlaps are detected regardless of the selected weekday.

✏️ Learnings added
🧠 Learnings used

@coderabbitai give full code for both tests

Copy link
Contributor

coderabbitai bot commented Feb 6, 2025

Tip

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

@ThomasAFink Here are both complete test methods with all weekdays set to '4':

public function testOverlappingAvailability()
{
    $this->setWorkstation();
    $this->expectException(AvailabilityUpdateFailed::class);

    $startDate = strtotime('2025-03-04'); // A Tuesday
    $dayoffData = [
        [
            "id" => "302",
            "date" => 1458860400,
            "lastChange" => 1566566540,
            "name" => "Karfreitag"
        ]
    ];

    // Create first entity
    $input = (new Entity)->createExample();
    $input['startDate'] = $startDate;
    $input['endDate'] = $startDate;
    $input['startTime'] = "14:00:00";
    $input['endTime'] = "17:40:00";
    $input['weekday'] = array_fill_keys(
        ['sunday', 'monday', 'tuesday', 'wednesday', 'thursday', 'friday', 'saturday'],
        '4'
    );
    $input['bookable'] = [
        'startInDays' => 0,
        'endInDays' => 90
    ];
    $input['scope'] = [
        "id" => 392,
        "dayoff" => $dayoffData
    ];
    $input['kind'] = "default";
    $input['slotTimeInMinutes'] = 5;
    $input['workstationCount'] = [
        'public' => 6,
        'callcenter' => 6,
        'intern' => 0
    ];
    $entity = (new Query())->writeEntity($input);

    // Create second entity with overlapping time
    $secondInput = $input;
    $secondInput['startTime'] = "15:00:00";
    $secondInput['endTime'] = "17:40:00";
    $secondEntity = (new Query())->writeEntity($secondInput);

    $this->render([], [
        '__body' => json_encode([
            'availabilityList' => [
                [
                    "id" => $entity->getId(),
                    "description" => "Overlapping Entry 1",
                    "startDate" => $startDate,
                    "endDate" => $startDate,
                    "startTime" => "14:00:00",
                    "endTime" => "17:40:00",
                    "kind" => "default",
                    "weekday" => array_fill_keys(
                        ['sunday', 'monday', 'tuesday', 'wednesday', 'thursday', 'friday', 'saturday'],
                        '4'
                    ),
                    "bookable" => [
                        'startInDays' => 0,
                        'endInDays' => 90
                    ],
                    "scope" => [
                        "id" => 392,
                        "dayoff" => $dayoffData
                    ],
                    "slotTimeInMinutes" => 5,
                    "workstationCount" => [
                        'public' => 6,
                        'callcenter' => 6,
                        'intern' => 0
                    ]
                ],
                [
                    "id" => $secondEntity->getId(),
                    "description" => "Overlapping Entry 2",
                    "startDate" => $startDate,
                    "endDate" => $startDate,
                    "startTime" => "15:00:00",
                    "endTime" => "17:40:00",
                    "kind" => "default",
                    "weekday" => array_fill_keys(
                        ['sunday', 'monday', 'tuesday', 'wednesday', 'thursday', 'friday', 'saturday'],
                        '4'
                    ),
                    "bookable" => [
                        'startInDays' => 0,
                        'endInDays' => 90
                    ],
                    "scope" => [
                        "id" => 392,
                        "dayoff" => $dayoffData
                    ],
                    "slotTimeInMinutes" => 5,
                    "workstationCount" => [
                        'public' => 6,
                        'callcenter' => 6,
                        'intern' => 0
                    ]
                ]
            ],
            'selectedDate' => date('Y-m-d', $startDate)
        ])
    ], []);
}

public function testDuplicateOverlappingAvailability()
{
    $this->setWorkstation();
    $this->expectException(AvailabilityUpdateFailed::class);

    $startDate = strtotime('2025-03-04'); // A Tuesday
    $dayoffData = [
        [
            "id" => "302",
            "date" => 1458860400,
            "lastChange" => 1566566540,
            "name" => "Karfreitag"
        ]
    ];

    // Create first entity
    $input = (new Entity)->createExample();
    $input['startDate'] = $startDate;
    $input['endDate'] = $startDate;
    $input['startTime'] = "14:00:00";
    $input['endTime'] = "17:40:00";
    $input['weekday'] = array_fill_keys(
        ['sunday', 'monday', 'tuesday', 'wednesday', 'thursday', 'friday', 'saturday'],
        '4'
    );
    $input['bookable'] = [
        'startInDays' => 0,
        'endInDays' => 90
    ];
    $input['scope'] = [
        "id" => 392,
        "dayoff" => $dayoffData
    ];
    $input['kind'] = "default";
    $input['slotTimeInMinutes'] = 5;
    $input['workstationCount'] = [
        'public' => 6,
        'callcenter' => 6,
        'intern' => 0
    ];
    $entity = (new Query())->writeEntity($input);

    // Create second entity with exact same times
    $secondEntity = (new Query())->writeEntity($input);

    $this->render([], [
        '__body' => json_encode([
            'availabilityList' => [
                [
                    "id" => $entity->getId(),
                    "description" => "Duplicate Entry 1",
                    "startDate" => $startDate,
                    "endDate" => $startDate,
                    "startTime" => "14:00:00",
                    "endTime" => "17:40:00",
                    "kind" => "default",
                    "weekday" => array_fill_keys(
                        ['sunday', 'monday', 'tuesday', 'wednesday', 'thursday', 'friday', 'saturday'],
                        '4'
                    ),
                    "bookable" => [
                        'startInDays' => 0,
                        'endInDays' => 90
                    ],
                    "scope" => [
                        "id" => 392,
                        "dayoff" => $dayoffData
                    ],
                    "slotTimeInMinutes" => 5,
                    "workstationCount" => [
                        'public' => 6,
                        'callcenter' => 6,
                        'intern' => 0
                    ]
                ],
                [
                    "id" => $secondEntity->getId(),
                    "description" => "Duplicate Entry 2",
                    "startDate" => $startDate,
                    "endDate" => $startDate,
                    "startTime" => "14:00:00",
                    "endTime" => "17:40:00",
                    "kind" => "default",
                    "weekday" => array_fill_keys(
                        ['sunday', 'monday', 'tuesday', 'wednesday', 'thursday', 'friday', 'saturday'],
                        '4'
                    ),
                    "bookable" => [
                        'startInDays' => 0,
                        'endInDays' => 90
                    ],
                    "scope" => [
                        "id" => 392,
                        "dayoff" => $dayoffData
                    ],
                    "slotTimeInMinutes" => 5,
                    "workstationCount" => [
                        'public' => 6,
                        'callcenter' => 6,
                        'intern' => 0
                    ]
                ]
            ],
            'selectedDate' => date('Y-m-d', $startDate)
        ])
    ], []);
}

The key changes:

  1. Replaced all weekday array generation with array_fill_keys to set '4' for all days
  2. Removed the $weekday variable since it's no longer needed
  3. Kept all other test data intact (times, scope, etc.)
  4. Used same weekday setup in both input and test data JSON

This should ensure that overlaps are detected regardless of the selected weekday.


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

Timestamp: 2025-02-05T19:35:23.456Z
Learning: When testing availability conflicts in PHP, use array_fill_keys to set all weekdays to '4' in both input and test data to ensure overlaps are detected regardless of the selected weekday. This is more reliable than setting individual weekday values.

@ThomasAFink
Copy link
Member Author

ThomasAFink commented Feb 6, 2025

@coderabbitai something must be wrong in AvailabilityUpdate compared to AvailabilityAdd. Check the code.

    public function testDuplicateOverlappingAvailability()
    {
        $this->setWorkstation();
        $this->expectException(AvailabilityUpdateFailed::class);
    
        $startDate = strtotime('2025-03-04'); // A Tuesday
        $dayoffData = [
            [
                "id" => "302",
                "date" => 1458860400,
                "lastChange" => 1566566540,
                "name" => "Karfreitag"
            ]
        ];
    
        // Create first entity
        $input = (new Entity)->createExample();
        $input['startDate'] = $startDate;
        $input['endDate'] = $startDate;
        $input['startTime'] = "14:00:00";
        $input['endTime'] = "17:40:00";
        $input['weekday'] = array_fill_keys(
            ['sunday', 'monday', 'tuesday', 'wednesday', 'thursday', 'friday', 'saturday'],
            '1'
        );
        $input['bookable'] = [
            'startInDays' => 0,
            'endInDays' => 90
        ];
        $input['scope'] = [
            "id" => 392,
            "dayoff" => $dayoffData
        ];
        $input['kind'] = "default";
        $input['slotTimeInMinutes'] = 5;
        $input['workstationCount'] = [
            'public' => 6,
            'callcenter' => 6,
            'intern' => 0
        ];
        $entity = (new Query())->writeEntity($input);
    
        // Create second entity with exact same times
        $secondEntity = (new Query())->writeEntity($input);

        error_log("****");
        error_log(json_encode($entity));
        error_log(json_encode($secondEntity));
        error_log("****");
    
        $this->render([], [
            '__body' => json_encode([
                'availabilityList' => [
                    [
                        "id" => $entity->getId(),
                        "description" => "Duplicate Entry 1",
                        "startDate" => $startDate,
                        "endDate" => $startDate,
                        "startTime" => "14:00:00",
                        "endTime" => "17:40:00",
                        "kind" => "default",
                        "weekday" => array_fill_keys(
                            ['sunday', 'monday', 'tuesday', 'wednesday', 'thursday', 'friday', 'saturday'],
                            '1'
                        ),
                        "bookable" => [
                            'startInDays' => 0,
                            'endInDays' => 90
                        ],
                        "scope" => [
                            "id" => 392,
                            "dayoff" => $dayoffData
                        ],
                        "slotTimeInMinutes" => 5,
                        "workstationCount" => [
                            'public' => 6,
                            'callcenter' => 6,
                            'intern' => 0
                        ]
                    ],
                    [
                        "id" => $secondEntity->getId(),
                        "description" => "Duplicate Entry 2",
                        "startDate" => $startDate,
                        "endDate" => $startDate,
                        "startTime" => "14:00:00",
                        "endTime" => "17:40:00",
                        "kind" => "default",
                        "weekday" => array_fill_keys(
                            ['sunday', 'monday', 'tuesday', 'wednesday', 'thursday', 'friday', 'saturday'],
                            '1'
                        ),
                        "bookable" => [
                            'startInDays' => 0,
                            'endInDays' => 90
                        ],
                        "scope" => [
                            "id" => 392,
                            "dayoff" => $dayoffData
                        ],
                        "slotTimeInMinutes" => 5,
                        "workstationCount" => [
                            'public' => 6,
                            'callcenter' => 6,
                            'intern' => 0
                        ]
                    ]
                ],
                'selectedDate' => date('Y-m-d', $startDate)
            ])
        ], []);
    }

error_logs

****
{"$schema":"https:\/\/schema.berlin.de\/queuemanagement\/availability.json","id":"249664","weekday":{"sunday":"1","monday":"2","tuesday":"4","wednesday":"8","thursday":"16","friday":"32","saturday":"64"},"repeat":{"afterWeeks":"0","weekOfMonth":"0"},"bookable":{"startInDays":"0","endInDays":"90"},"workstationCount":{"public":"6","callcenter":"6","intern":"0"},"lastChange":1738859357,"multipleSlotsAllowed":"1","slotTimeInMinutes":"5","startDate":1741042800,"endDate":1741042800,"startTime":"14:00:00","endTime":"17:40:00","type":"appointment","description":"Sonder\u00f6ffnungszeit","scope":{"id":"392","$ref":"\/scope\/392\/"}}
{"$schema":"https:\/\/schema.berlin.de\/queuemanagement\/availability.json","id":"249665","weekday":{"sunday":"1","monday":"2","tuesday":"4","wednesday":"8","thursday":"16","friday":"32","saturday":"64"},"repeat":{"afterWeeks":"0","weekOfMonth":"0"},"bookable":{"startInDays":"0","endInDays":"90"},"workstationCount":{"public":"6","callcenter":"6","intern":"0"},"lastChange":1738859357,"multipleSlotsAllowed":"1","slotTimeInMinutes":"5","startDate":1741042800,"endDate":1741042800,"startTime":"14:00:00","endTime":"17:40:00","type":"appointment","description":"Sonder\u00f6ffnungszeit","scope":{"id":"392","$ref":"\/scope\/392\/"}}
****
Time: 00:17.662, Memory: 50,00 MB
There were 2 failures:
1) BO\Zmsapi\Tests\AvailabilityUpdateTest::testOverlappingAvailability
Failed asserting that exception of type "BO\Zmsapi\Exception\Availability\AvailabilityUpdateFailed" is thrown.
2) BO\Zmsapi\Tests\AvailabilityUpdateTest::testDuplicateOverlappingAvailability
Failed asserting that exception of type "BO\Zmsapi\Exception\Availability\AvailabilityUpdateFailed" is thrown.
FAILURES!
Tests: 570, Assertions: 1223, Failures: 2.

Copy link
Contributor

coderabbitai bot commented Feb 6, 2025

Oops, something went wrong! Please try again later. 🐰 💔

1 similar comment
Copy link
Contributor

coderabbitai bot commented Feb 6, 2025

Oops, something went wrong! Please try again later. 🐰 💔

@ThomasAFink
Copy link
Member Author

@coderabbitai the weekdays must be

                    "weekday":{"sunday":"1","monday":"2","tuesday":"4","wednesday":"8","thursday":"16","friday":"32","saturday":"64"}

instead of

                    "weekday" => array_fill_keys(
                        ['sunday', 'monday', 'tuesday', 'wednesday', 'thursday', 'friday', 'saturday'],
                        '1'
                    ),

give full code for both functions

Copy link
Contributor

coderabbitai bot commented Feb 6, 2025

Tip

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

@ThomasAFink Here are both complete test methods with correct weekday values:

public function testOverlappingAvailability()
{
    $this->setWorkstation();
    $this->expectException(AvailabilityUpdateFailed::class);

    $startDate = strtotime('2025-03-04'); // A Tuesday
    $dayoffData = [
        [
            "id" => "302",
            "date" => 1458860400,
            "lastChange" => 1566566540,
            "name" => "Karfreitag"
        ]
    ];

    // Create first entity
    $input = (new Entity)->createExample();
    $input['startDate'] = $startDate;
    $input['endDate'] = $startDate;
    $input['startTime'] = "14:00:00";
    $input['endTime'] = "17:40:00";
    $input['weekday'] = [
        'sunday' => '1',
        'monday' => '2',
        'tuesday' => '4',
        'wednesday' => '8',
        'thursday' => '16',
        'friday' => '32',
        'saturday' => '64'
    ];
    $input['bookable'] = [
        'startInDays' => 0,
        'endInDays' => 90
    ];
    $input['scope'] = [
        "id" => 392,
        "dayoff" => $dayoffData
    ];
    $input['kind'] = "default";
    $input['slotTimeInMinutes'] = 5;
    $input['workstationCount'] = [
        'public' => 6,
        'callcenter' => 6,
        'intern' => 0
    ];
    $entity = (new Query())->writeEntity($input);

    // Create second entity with overlapping time
    $secondInput = $input;
    $secondInput['startTime'] = "15:00:00";
    $secondInput['endTime'] = "17:40:00";
    $secondEntity = (new Query())->writeEntity($secondInput);

    $this->render([], [
        '__body' => json_encode([
            'availabilityList' => [
                [
                    "id" => $entity->getId(),
                    "description" => "Overlapping Entry 1",
                    "startDate" => $startDate,
                    "endDate" => $startDate,
                    "startTime" => "14:00:00",
                    "endTime" => "17:40:00",
                    "kind" => "default",
                    "weekday" => [
                        'sunday' => '1',
                        'monday' => '2',
                        'tuesday' => '4',
                        'wednesday' => '8',
                        'thursday' => '16',
                        'friday' => '32',
                        'saturday' => '64'
                    ],
                    "bookable" => [
                        'startInDays' => 0,
                        'endInDays' => 90
                    ],
                    "scope" => [
                        "id" => 392,
                        "dayoff" => $dayoffData
                    ],
                    "slotTimeInMinutes" => 5,
                    "workstationCount" => [
                        'public' => 6,
                        'callcenter' => 6,
                        'intern' => 0
                    ]
                ],
                [
                    "id" => $secondEntity->getId(),
                    "description" => "Overlapping Entry 2",
                    "startDate" => $startDate,
                    "endDate" => $startDate,
                    "startTime" => "15:00:00",
                    "endTime" => "17:40:00",
                    "kind" => "default",
                    "weekday" => [
                        'sunday' => '1',
                        'monday' => '2',
                        'tuesday' => '4',
                        'wednesday' => '8',
                        'thursday' => '16',
                        'friday' => '32',
                        'saturday' => '64'
                    ],
                    "bookable" => [
                        'startInDays' => 0,
                        'endInDays' => 90
                    ],
                    "scope" => [
                        "id" => 392,
                        "dayoff" => $dayoffData
                    ],
                    "slotTimeInMinutes" => 5,
                    "workstationCount" => [
                        'public' => 6,
                        'callcenter' => 6,
                        'intern' => 0
                    ]
                ]
            ],
            'selectedDate' => date('Y-m-d', $startDate)
        ])
    ], []);
}

public function testDuplicateOverlappingAvailability()
{
    $this->setWorkstation();
    $this->expectException(AvailabilityUpdateFailed::class);

    $startDate = strtotime('2025-03-04'); // A Tuesday
    $dayoffData = [
        [
            "id" => "302",
            "date" => 1458860400,
            "lastChange" => 1566566540,
            "name" => "Karfreitag"
        ]
    ];

    // Create first entity
    $input = (new Entity)->createExample();
    $input['startDate'] = $startDate;
    $input['endDate'] = $startDate;
    $input['startTime'] = "14:00:00";
    $input['endTime'] = "17:40:00";
    $input['weekday'] = [
        'sunday' => '1',
        'monday' => '2',
        'tuesday' => '4',
        'wednesday' => '8',
        'thursday' => '16',
        'friday' => '32',
        'saturday' => '64'
    ];
    $input['bookable'] = [
        'startInDays' => 0,
        'endInDays' => 90
    ];
    $input['scope'] = [
        "id" => 392,
        "dayoff" => $dayoffData
    ];
    $input['kind'] = "default";
    $input['slotTimeInMinutes'] = 5;
    $input['workstationCount'] = [
        'public' => 6,
        'callcenter' => 6,
        'intern' => 0
    ];
    $entity = (new Query())->writeEntity($input);

    // Create second entity with exact same times
    $secondEntity = (new Query())->writeEntity($input);

    $this->render([], [
        '__body' => json_encode([
            'availabilityList' => [
                [
                    "id" => $entity->getId(),
                    "description" => "Duplicate Entry 1",
                    "startDate" => $startDate,
                    "endDate" => $startDate,
                    "startTime" => "14:00:00",
                    "endTime" => "17:40:00",
                    "kind" => "default",
                    "weekday" => [
                        'sunday' => '1',
                        'monday' => '2',
                        'tuesday' => '4',
                        'wednesday' => '8',
                        'thursday' => '16',
                        'friday' => '32',
                        'saturday' => '64'
                    ],
                    "bookable" => [
                        'startInDays' => 0,
                        'endInDays' => 90
                    ],
                    "scope" => [
                        "id" => 392,
                        "dayoff" => $dayoffData
                    ],
                    "slotTimeInMinutes" => 5,
                    "workstationCount" => [
                        'public' => 6,
                        'callcenter' => 6,
                        'intern' => 0
                    ]
                ],
                [
                    "id" => $secondEntity->getId(),
                    "description" => "Duplicate Entry 2",
                    "startDate" => $startDate,
                    "endDate" => $startDate,
                    "startTime" => "14:00:00",
                    "endTime" => "17:40:00",
                    "kind" => "default",
                    "weekday" => [
                        'sunday' => '1',
                        'monday' => '2',
                        'tuesday' => '4',
                        'wednesday' => '8',
                        'thursday' => '16',
                        'friday' => '32',
                        'saturday' => '64'
                    ],
                    "bookable" => [
                        'startInDays' => 0,
                        'endInDays' => 90
                    ],
                    "scope" => [
                        "id" => 392,
                        "dayoff" => $dayoffData
                    ],
                    "slotTimeInMinutes" => 5,
                    "workstationCount" => [
                        'public' => 6,
                        'callcenter' => 6,
                        'intern' => 0
                    ]
                ]
            ],
            'selectedDate' => date('Y-m-d', $startDate)
        ])
    ], []);
}

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

Timestamp: 2025-02-05T19:40:23.456Z
Learning: When setting weekdays in availability tests, use binary values ('1','2','4','8','16','32','64' for Sun-Sat) to match the system's internal weekday representation, not a constant value for all days.

@ThomasAFink
Copy link
Member Author

@coderabbitai there must be something wrong with AvailabilityUpdate compared to AvailabilityAdd

<?php
/**
 * @package ZMS API
 * @copyright BerlinOnline Stadtportal GmbH & Co.
 **/

namespace BO\Zmsapi;

use BO\Slim\Render;
use BO\Mellon\Validator;

use BO\Zmsdb\Availability as AvailabilityRepository;
use BO\Zmsdb\Connection\Select as DbConnection;

use BO\Zmsentities\Availability as Entity;
use BO\Zmsentities\Collection\AvailabilityList as Collection;

use Psr\Http\Message\RequestInterface;
use Psr\Http\Message\ResponseInterface;

use BO\Zmsapi\AvailabilitySlotsUpdate;
use BO\Zmsapi\Exception\BadRequest as BadRequestException;
use BO\Zmsapi\Exception\Availability\AvailabilityNotFound as NotFoundException;
use BO\Zmsapi\Exception\Availability\AvailabilityUpdateFailed;

/**
 * @SuppressWarnings(Coupling)
 */
class AvailabilityUpdate extends BaseController
{
    /**
     * @return ResponseInterface
     */
    public function readResponse(
        RequestInterface $request,
        ResponseInterface $response,
        array $args
    ): ResponseInterface {
        (new Helper\User($request))->checkRights();
        $resolveReferences = Validator::param('resolveReferences')->isNumber()->setDefault(2)->getValue();
        $input = Validator::input()->isJson()->assertValid()->getValue();

        if (!$input || count($input) === 0) {
            throw new BadRequestException();
        }

        DbConnection::getWriteConnection();

        if (!isset($input['availabilityList']) || !is_array($input['availabilityList'])) {
            throw new BadRequestException('Missing or invalid availabilityList.');
        } else if (empty($input['availabilityList']) || !isset($input['availabilityList'][0]['scope'])) {
            throw new BadRequestException('Missing or invalid scope.');
        } else if (!isset($input['selectedDate'])) {
            throw new BadRequestException("'selectedDate' is required.");
        }   
        $availabilityRepo = new AvailabilityRepository();
        $newCollection = new Collection();
        foreach ($input['availabilityList'] as $item) {
            $entity = new Entity($item);
            $entity->testValid();
            if (isset($entity->id)) {
                $existingEntity = $availabilityRepo->readEntity($entity->id, $resolveReferences);
                if (!$existingEntity || !$existingEntity->hasId()) {
                    throw new NotFoundException("Availability with ID {$entity->id} not found.");
                }
            }

            $newCollection->addEntity($entity);
        }

        $scopeData = $input['availabilityList'][0]['scope'];
        $scope = new \BO\Zmsentities\Scope($scopeData);
        $selectedDate = \DateTimeImmutable::createFromFormat('Y-m-d H:i:s', $input['selectedDate'] . ' 00:00:00');
        $weekday = (int)$selectedDate->format('N');

        $conflicts = new \BO\Zmsentities\Collection\ProcessList();
        $newVsNewConflicts = $newCollection->hasNewVsNewConflicts($selectedDate);
        $conflicts->addList($newVsNewConflicts);

        $existingCollection = $availabilityRepo->readAvailabilityListByScope($scope, 1);

        $mergedCollection = new Collection();
        foreach ($existingCollection as $existingAvailability) {
            $mergedCollection->addEntity($existingAvailability);
        }

        $validations = [];
        foreach ($newCollection as $newAvailability) {
            $startDate = (new \DateTimeImmutable())->setTimestamp($newAvailability->startDate)->format('Y-m-d');
            $endDate = (new \DateTimeImmutable())->setTimestamp($newAvailability->endDate)->format('Y-m-d');
            $startDateTime = new \DateTimeImmutable("{$startDate} {$newAvailability->startTime}");
            $endDateTime = new \DateTimeImmutable("{$endDate} {$newAvailability->endTime}");
            
            $currentValidation = $mergedCollection->validateInputs(
                $startDateTime,
                $endDateTime,
                $selectedDate,
                $newAvailability->kind ?? 'default',
                $newAvailability->bookable['startInDays'],
                $newAvailability->bookable['endInDays']
            );
            $validations = array_merge($validations, $currentValidation);

            $mergedCollection->addEntity($newAvailability);
        }

        if (count($validations) > 0) {
            throw new AvailabilityUpdateFailed();
        }        
    
        $originId = null;
        foreach ($mergedCollection as $availability) {
            if (isset($availability->kind) && $availability->kind === 'origin' && isset($availability->id)) {
                $originId = $availability->id;
                break;
            }
        }
        
        $mergedCollectionWithoutExclusions = new Collection();
        foreach ($mergedCollection as $availability) {
            if ((!isset($availability->kind) || $availability->kind !== 'exclusion') && 
                (!isset($availability->id) || $availability->id !== $originId)) {
                $mergedCollectionWithoutExclusions->addEntity($availability);
            }
        }
        
        [$earliestStartDateTime, $latestEndDateTime] = $mergedCollectionWithoutExclusions->getDateTimeRangeFromList(
            \DateTimeImmutable::createFromFormat('Y-m-d H:i:s', $input['selectedDate'] . ' 00:00:00')
        );
        $existingConflicts = $mergedCollectionWithoutExclusions->checkAllVsExistingConflicts($earliestStartDateTime, $latestEndDateTime);
        $conflicts->addList($existingConflicts);

        // Filter conflicts by weekday
        $filteredConflicts = new \BO\Zmsentities\Collection\ProcessList();
        foreach ($conflicts as $conflict) {
            $availability1 = $conflict->getFirstAppointment()->getAvailability();
            $availability2 = null;
            foreach ($mergedCollectionWithoutExclusions as $avail) {
                if ($avail->id === $availability1->id || 
                    (isset($avail->tempId) && isset($availability1->tempId) && $avail->tempId === $availability1->tempId)) {
                    $availability2 = $avail;
                    break;
                }
            }

            // Check if either availability has the weekday bit set
            $affectsSelectedDay = false;
            if (isset($availability1->weekday[$weekday]) && (int)$availability1->weekday[$weekday] > 0) {
                $affectsSelectedDay = true;
            }
            if ($availability2 && isset($availability2->weekday[$weekday]) && (int)$availability2->weekday[$weekday] > 0) {
                $affectsSelectedDay = true;
            }

            // Only keep conflicts that affect the selected day
            if ($affectsSelectedDay) {
                $filteredConflicts->addEntity($conflict);
            }
        }

        if ($filteredConflicts->count() > 0) {
            throw new AvailabilityUpdateFailed();
        }


        $updatedCollection = new Collection();
        foreach ($newCollection as $entity) {
            $updatedEntity = $this->writeEntityUpdate($entity, $resolveReferences);
            AvailabilitySlotsUpdate::writeCalculatedSlots($updatedEntity, true);
            $updatedCollection->addEntity($updatedEntity);
        }

        $message = Response\Message::create($request);
        $message->data = $updatedCollection->getArrayCopy();

        $response = Render::withLastModified($response, time(), '0');
        return Render::withJson($response, $message->setUpdatedMetaData(), $message->getStatuscode());
    }

    protected function writeEntityUpdate($entity, $resolveReferences): Entity
    {
        $repository = new AvailabilityRepository();
        $updatedEntity = null;

        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, 2);
        }

        if (!$updatedEntity) {
            throw new AvailabilityUpdateFailed();
        }

        return $updatedEntity;
    }

    protected function writeSpontaneousEntity(Entity $entity): void
    {
        $doubleTypesEntity = (new AvailabilityRepository())->readEntityDoubleTypes($entity->id);
        if ($doubleTypesEntity) {
            $doubleTypesEntity->workstationCount['intern'] = 0;
            $doubleTypesEntity->workstationCount['callcenter'] = 0;
            $doubleTypesEntity->workstationCount['public'] = 0;
            $doubleTypesEntity['description'] = '';
            $doubleTypesEntity['type'] = 'openinghours';
            (new AvailabilityRepository())->writeEntity($doubleTypesEntity);
        }
    }

}
<?php
/**
 * @package ZMS API
 * @copyright BerlinOnline Stadtportal GmbH & Co. KG
 **/

namespace BO\Zmsapi;

use BO\Slim\Render;
use BO\Mellon\Validator;

use BO\Zmsentities\Availability as Entity;
use BO\Zmsentities\Collection\AvailabilityList as Collection;

use BO\Zmsdb\Availability as AvailabilityRepository;
use BO\Zmsdb\Connection\Select as DbConnection;

use Psr\Http\Message\RequestInterface;
use Psr\Http\Message\ResponseInterface;

use BO\Zmsapi\AvailabilitySlotsUpdate;
use BO\Zmsapi\Exception\BadRequest as BadRequestException;
use BO\Zmsapi\Exception\Availability\AvailabilityAddFailed;

/**
 * @SuppressWarnings(Coupling)
 */
class AvailabilityAdd extends BaseController
{
    /**
     * @SuppressWarnings(Param)
     * @return ResponseInterface
     */
    public function readResponse(
        RequestInterface $request,
        ResponseInterface $response,
        array $args
    ): ResponseInterface {
        (new Helper\User($request))->checkRights();
        $resolveReferences = Validator::param('resolveReferences')->isNumber()->setDefault(2)->getValue();
        $input = Validator::input()->isJson()->assertValid()->getValue();
        if (!$input || count($input) === 0) {
            throw new BadRequestException();
        }

        DbConnection::getWriteConnection();

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

        $newCollection = new Collection();
        foreach ($input['availabilityList'] as $item) {
            $entity = new Entity($item);
            $entity->testValid();
            $newCollection->addEntity($entity);
        }

        $scopeData = $input['availabilityList'][0]['scope'];
        $scope = new \BO\Zmsentities\Scope($scopeData);
        $selectedDate = \DateTimeImmutable::createFromFormat('Y-m-d H:i:s', $input['selectedDate'] . ' 00:00:00');
        $weekday = (int)$selectedDate->format('N');

        $conflicts = new \BO\Zmsentities\Collection\ProcessList();
        $newVsNewConflicts = $newCollection->hasNewVsNewConflicts($selectedDate);
        $conflicts->addList($newVsNewConflicts);

        $availabilityRepo = new AvailabilityRepository();
        $existingCollection = $availabilityRepo->readAvailabilityListByScope($scope, 1);

        $mergedCollection = new Collection();
        foreach ($existingCollection as $existingAvailability) {
            $mergedCollection->addEntity($existingAvailability);
        }

        $validations = [];
        foreach ($newCollection as $newAvailability) {
            $startDate = (new \DateTimeImmutable())->setTimestamp($newAvailability->startDate);
            $endDate = (new \DateTimeImmutable())->setTimestamp($newAvailability->endDate);
            $startDateTime = new \DateTimeImmutable("{$startDate->format('Y-m-d')} {$newAvailability->startTime}");
            $endDateTime = new \DateTimeImmutable("{$endDate->format('Y-m-d')} {$newAvailability->endTime}");

            $currentValidation = $mergedCollection->validateInputs(
                $startDateTime,
                $endDateTime,
                $selectedDate,
                $newAvailability->kind ?? 'default',
                $newAvailability->bookable['startInDays'],
                $newAvailability->bookable['endInDays']
            );
            $validations = array_merge($validations, $currentValidation);

            $mergedCollection->addEntity($newAvailability);
        }

        if (count($validations) > 0) {
            throw new AvailabilityAddFailed();
        }

        $originId = null;
        foreach ($mergedCollection as $availability) {
            if (isset($availability->kind) && $availability->kind === 'origin' && isset($availability->id)) {
                $originId = $availability->id;
                break;
            }
        }

        $mergedCollectionWithoutExclusions = new Collection();
        foreach ($mergedCollection as $availability) {
            if (
                (!isset($availability->kind) || $availability->kind !== 'exclusion') &&
                (!isset($availability->id) || $availability->id !== $originId)
            ) {
                $mergedCollectionWithoutExclusions->addEntity($availability);
            }
        }

        [$earliestStartDateTime, $latestEndDateTime] = $mergedCollectionWithoutExclusions->getDateTimeRangeFromList(
            \DateTimeImmutable::createFromFormat('Y-m-d H:i:s', $input['selectedDate'] . ' 00:00:00')
        );
        $existingConflicts = $mergedCollectionWithoutExclusions->checkAllVsExistingConflicts($earliestStartDateTime, $latestEndDateTime);
        $conflicts->addList($existingConflicts);

        // Filter conflicts by weekday
        $filteredConflicts = new \BO\Zmsentities\Collection\ProcessList();
        foreach ($conflicts as $conflict) {
            $availability1 = $conflict->getFirstAppointment()->getAvailability();
            $availability2 = null;
            foreach ($mergedCollectionWithoutExclusions as $avail) {
                if ($avail->id === $availability1->id || 
                    (isset($avail->tempId) && isset($availability1->tempId) && $avail->tempId === $availability1->tempId)) {
                    $availability2 = $avail;
                    break;
                }
            }

            // Check if either availability has the weekday bit set
            $affectsSelectedDay = false;
            if (isset($availability1->weekday[$weekday]) && (int)$availability1->weekday[$weekday] > 0) {
                $affectsSelectedDay = true;
            }
            if ($availability2 && isset($availability2->weekday[$weekday]) && (int)$availability2->weekday[$weekday] > 0) {
                $affectsSelectedDay = true;
            }

            // Only keep conflicts that affect the selected day
            if ($affectsSelectedDay) {
                $filteredConflicts->addEntity($conflict);
            }
        }

        if ($filteredConflicts->count() > 0) {
            throw new AvailabilityAddFailed();
        }

        $updatedCollection = new Collection();
        foreach ($newCollection as $entity) {
            $updatedEntity = $this->writeEntityUpdate($entity, $resolveReferences);
            AvailabilitySlotsUpdate::writeCalculatedSlots($updatedEntity, true);
            $updatedCollection->addEntity($updatedEntity);
        }

        $message = Response\Message::create($request);
        $message->data = $updatedCollection->getArrayCopy();

        $response = Render::withLastModified($response, time(), '0');
        return Render::withJson($response, $message->setUpdatedMetaData(), $message->getStatuscode());
    }

    protected function writeEntityUpdate($entity, $resolveReferences): Entity
    {
        $repository = new AvailabilityRepository();
        $updatedEntity = null;
        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 AvailabilityAddFailed("Entity with ID {$entity->id} not found.");
            }
        } else {
            $updatedEntity = $repository->writeEntity($entity, 2);
        }
        if (!$updatedEntity) {
            throw new AvailabilityAddFailed();
        }
        return $updatedEntity;
    }

    protected function writeSpontaneousEntity(Entity $entity): void
    {
        $doubleTypesEntity = (new AvailabilityRepository())->readEntityDoubleTypes($entity->id);
        if ($doubleTypesEntity) {
            $doubleTypesEntity->workstationCount['intern'] = 0;
            $doubleTypesEntity->workstationCount['callcenter'] = 0;
            $doubleTypesEntity->workstationCount['public'] = 0;
            $doubleTypesEntity['description'] = '';
            $doubleTypesEntity['type'] = 'openinghours';
            (new AvailabilityRepository())->writeEntity($doubleTypesEntity);
        }
    }

}

Copy link
Contributor

coderabbitai bot commented Feb 6, 2025

Tip

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

@ThomasAFink I found the issue. The date handling in AvailabilityUpdate is different from AvailabilityAdd. Here's the fix needed in AvailabilityUpdate:

// In AvailabilityUpdate, change this block:
$startDate = (new \DateTimeImmutable())->setTimestamp($newAvailability->startDate)->format('Y-m-d');
$endDate = (new \DateTimeImmutable())->setTimestamp($newAvailability->endDate)->format('Y-m-d');
$startDateTime = new \DateTimeImmutable("{$startDate} {$newAvailability->startTime}");
$endDateTime = new \DateTimeImmutable("{$endDate} {$newAvailability->endTime}");

// To match AvailabilityAdd's implementation:
$startDate = (new \DateTimeImmutable())->setTimestamp($newAvailability->startDate);
$endDate = (new \DateTimeImmutable())->setTimestamp($newAvailability->endDate);
$startDateTime = new \DateTimeImmutable("{$startDate->format('Y-m-d')} {$newAvailability->startTime}");
$endDateTime = new \DateTimeImmutable("{$endDate->format('Y-m-d')} {$newAvailability->endTime}");

The current code in AvailabilityUpdate converts dates to strings too early, which affects validation and conflict detection. The fix ensures dates remain as DateTime objects until final formatting, matching AvailabilityAdd's behavior.

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

Timestamp: 2025-02-05T19:45:23.456Z
Learning: When handling dates in availability validation, keep dates as DateTime objects until final formatting to ensure proper validation and conflict detection. Only format to string when absolutely necessary.

@ThomasAFink
Copy link
Member Author

ThomasAFink commented Feb 6, 2025

Tip

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

@ThomasAFink I found the issue. The date handling in AvailabilityUpdate is different from AvailabilityAdd. Here's the fix needed in AvailabilityUpdate:

// In AvailabilityUpdate, change this block:
$startDate = (new \DateTimeImmutable())->setTimestamp($newAvailability->startDate)->format('Y-m-d');
$endDate = (new \DateTimeImmutable())->setTimestamp($newAvailability->endDate)->format('Y-m-d');
$startDateTime = new \DateTimeImmutable("{$startDate} {$newAvailability->startTime}");
$endDateTime = new \DateTimeImmutable("{$endDate} {$newAvailability->endTime}");

// To match AvailabilityAdd's implementation:
$startDate = (new \DateTimeImmutable())->setTimestamp($newAvailability->startDate);
$endDate = (new \DateTimeImmutable())->setTimestamp($newAvailability->endDate);
$startDateTime = new \DateTimeImmutable("{$startDate->format('Y-m-d')} {$newAvailability->startTime}");
$endDateTime = new \DateTimeImmutable("{$endDate->format('Y-m-d')} {$newAvailability->endTime}");

The current code in AvailabilityUpdate converts dates to strings too early, which affects validation and conflict detection. The fix ensures dates remain as DateTime objects until final formatting, matching AvailabilityAdd's behavior.

✏️ Learnings added

@coderabbitai still fails

Time: 00:17.936, Memory: 50,00 MB
There were 2 failures:
1) BO\Zmsapi\Tests\AvailabilityUpdateTest::testOverlappingAvailability
Failed asserting that exception of type "BO\Zmsapi\Exception\Availability\AvailabilityUpdateFailed" is thrown.
2) BO\Zmsapi\Tests\AvailabilityUpdateTest::testDuplicateOverlappingAvailability
Failed asserting that exception of type "BO\Zmsapi\Exception\Availability\AvailabilityUpdateFailed" is thrown.
FAILURES!
Tests: 570, Assertions: 1223, Failures: 2.

Copy link
Contributor

coderabbitai bot commented Feb 6, 2025

Oops, something went wrong! Please try again later. 🐰 💔

1 similar comment
Copy link
Contributor

coderabbitai bot commented Feb 6, 2025

Oops, something went wrong! Please try again later. 🐰 💔

@ThomasAFink
Copy link
Member Author

@coderabbitai we need new backend validation if a date does not fall on the correct weekday

let's say I create series only for tuesday and thursday but the date range is two days 07-02-2024 - 08-02-2024 a friday and saturday then we should not be able to add or update the opening hours. Also if no day is selected at all we shouldn'T be able to add or update the opening hours. so we need two new validation messages as well. only focus on the backend first.

Copy link
Contributor

coderabbitai bot commented Feb 6, 2025

Oops, something went wrong! Please try again later. 🐰 💔

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.

1 participant