Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
94.08% covered (success)
94.08%
143 / 152
83.33% covered (warning)
83.33%
10 / 12
CRAP
0.00% covered (danger)
0.00%
0 / 1
AvailabilityListUpdate
94.08% covered (success)
94.08%
143 / 152
83.33% covered (warning)
83.33%
10 / 12
34.24
0.00% covered (danger)
0.00%
0 / 1
 readResponse
100.00% covered (success)
100.00%
31 / 31
100.00% covered (success)
100.00%
1 / 1
2
 generateResponse
100.00% covered (success)
100.00%
8 / 8
100.00% covered (success)
100.00%
1 / 1
1
 updateAvailabilityList
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
2
 updateAvailability
100.00% covered (success)
100.00%
33 / 33
100.00% covered (success)
100.00%
1 / 1
5
 resetOpeningHours
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
2
 validateAvailabilityList
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
3
 validateAvailability
100.00% covered (success)
100.00%
17 / 17
100.00% covered (success)
100.00%
1 / 1
1
 createAvailabilityList
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
2
 createSelectedDateTime
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
1
 createMergedAvailabilityList
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
2
 validateClientData
40.00% covered (danger)
40.00%
4 / 10
0.00% covered (danger)
0.00%
0 / 1
17.58
 validateScopeConsistency
82.35% covered (warning)
82.35%
14 / 17
0.00% covered (danger)
0.00%
0 / 1
6.20
1<?php
2
3/**
4 * @package ZMS API
5 * @copyright BerlinOnline Stadtportal GmbH & Co. KG
6 **/
7
8namespace BO\Zmsapi;
9
10use BO\Slim\Render;
11use BO\Mellon\Validator;
12use BO\Zmsapi\Exception\Availability\AvailabilityListUpdateFailed;
13use BO\Zmsentities\Availability;
14use BO\Zmsentities\Collection\AvailabilityList;
15use BO\Zmsdb\Availability as AvailabilityRepository;
16use BO\Zmsdb\Connection\Select as DbConnection;
17use Psr\Http\Message\RequestInterface;
18use Psr\Http\Message\ResponseInterface;
19use BO\Zmsapi\AvailabilitySlotsUpdate;
20use BO\Zmsapi\Exception\BadRequest as BadRequestException;
21use App;
22
23/**
24 * @SuppressWarnings(Coupling)
25 */
26class AvailabilityListUpdate extends BaseController
27{
28    /**
29     * @SuppressWarnings(Param)
30     * @return ResponseInterface
31     */
32    #[\Override]
33    public function readResponse(
34        RequestInterface $request,
35        ResponseInterface $response,
36        array $args
37    ): ResponseInterface {
38        (new Helper\User($request))->checkPermissions('availability');
39        $input = Validator::input()->isJson()->assertValid()->getValue();
40        $resolveReferences = Validator::param('resolveReferences')->isNumber()->setDefault(2)->getValue();
41        self::validateClientData($input);
42        self::validateScopeConsistency($input['availabilityList']);
43        $newAvailabilities = $this->createAvailabilityList($input['availabilityList']);
44        $selectedDate = $this->createSelectedDateTime($input['selectedDate']);
45        $scope = new \BO\Zmsentities\Scope($input['availabilityList'][0]['scope']);
46        $existingAvailabilities = $this->createMergedAvailabilityList($scope);
47
48        $validationErrors = $this->validateAvailabilityList($newAvailabilities, $existingAvailabilities, $selectedDate);
49
50        if (count($validationErrors) > 0) {
51            $availabilityIds = array_map(
52                function ($availability) {
53                    return $availability->id ?? null;
54                },
55                $newAvailabilities->getArrayCopy()
56            );
57            App::$log->warning('AvailabilityListUpdateFailed: Validation failed', [
58                'ids' => array_filter($availabilityIds),
59                'scope_id' => $scope->getId(),
60                'errors' => $validationErrors
61            ]);
62            $message = Response\Message::create($request);
63            $message->data = [];
64            $message->meta->error = true;
65            $message->meta->message = json_encode(['errors' => $validationErrors]);
66            $message->meta->exception = 'BO\\Zmsapi\\Exception\\Availability\\AvailabilityListUpdateFailed';
67            return Render::withJson($response, $message, 400);
68        }
69
70        DbConnection::getWriteConnection();
71        $updatedAvailabilities = $this->updateAvailabilityList($newAvailabilities, $resolveReferences);
72        return $this->generateResponse($request, $response, $updatedAvailabilities);
73    }
74
75    private function generateResponse(
76        RequestInterface $request,
77        ResponseInterface $response,
78        AvailabilityList $availabilities
79    ): ResponseInterface {
80        $message = Response\Message::create($request);
81        $message->data = $availabilities->getArrayCopy();
82
83        $response = Render::withLastModified($response, time(), '0');
84        return Render::withJson(
85            $response,
86            $message->setUpdatedMetaData(),
87            $message->getStatuscode()
88        );
89    }
90
91    private function updateAvailabilityList(AvailabilityList $availabilities, int $resolveReferences): AvailabilityList
92    {
93        $updatedAvailabilities = new AvailabilityList();
94        foreach ($availabilities as $availability) {
95            $updatedAvailability = $this->updateAvailability($availability, $resolveReferences);
96            AvailabilitySlotsUpdate::writeCalculatedSlots($updatedAvailability, true);
97            $updatedAvailabilities->addEntity($updatedAvailability);
98        }
99        return $updatedAvailabilities;
100    }
101
102    protected function updateAvailability($availability, $resolveReferences): ?Availability
103    {
104        $repository = new AvailabilityRepository();
105        $updatedAvailability = null;
106        if ($availability->id) {
107            $existingAvailability = $repository->readEntity($availability->id);
108            if ($existingAvailability && $existingAvailability->hasId()) {
109                $availability->version = $existingAvailability->version + 1;
110                $this->resetOpeningHours($existingAvailability);
111                $updatedAvailability = $repository->updateEntity($availability->id, $availability, $resolveReferences);
112                App::$log->info('Updated availability', [
113                    'id' => $availability->id,
114                    'scope_id' => $availability->scope['id'],
115                    'version' => $availability->version,
116                    'startDate' => $availability->startDate,
117                    'endDate' => $availability->endDate,
118                    'startTime' => $availability->startTime,
119                    'endTime' => $availability->endTime,
120                    'type' => $availability->type,
121                    'operation' => 'update'
122                ]);
123            }
124        } else {
125            $updatedAvailability = $repository->writeEntity($availability, $resolveReferences);
126            App::$log->info('Created new availability', [
127                'id' => $updatedAvailability->id,
128                'scope_id' => $availability->scope['id'],
129                'startDate' => $availability->startDate,
130                'endDate' => $availability->endDate,
131                'startTime' => $availability->startTime,
132                'endTime' => $availability->endTime,
133                'type' => $availability->type,
134                'operation' => 'create'
135            ]);
136        }
137        if (!$updatedAvailability) {
138            throw new AvailabilityListUpdateFailed();
139        }
140        return $updatedAvailability;
141    }
142
143    protected function resetOpeningHours(Availability $availability): void
144    {
145        $doubleTypeAvailability = (new AvailabilityRepository())->readEntityDoubleTypes($availability->id);
146        if ($doubleTypeAvailability) {
147            $doubleTypeAvailability->workstationCount['intern'] = 0;
148            $doubleTypeAvailability->workstationCount['public'] = 0;
149            $doubleTypeAvailability['description'] = '';
150            $doubleTypeAvailability['type'] = 'openinghours';
151            (new AvailabilityRepository())->writeEntity($doubleTypeAvailability);
152        }
153    }
154
155    private function validateAvailabilityList(
156        AvailabilityList $newAvailabilities,
157        AvailabilityList $existingAvailabilities,
158        \DateTimeImmutable $selectedDate
159    ): array {
160        $validationErrors = [];
161        foreach ($newAvailabilities as $newAvailability) {
162            $errors = $this->validateAvailability($newAvailability, $existingAvailabilities, $selectedDate);
163            if (count($errors) > 0) {
164                $validationErrors = array_merge($validationErrors, $errors);
165            } else {
166                $existingAvailabilities->addEntity($newAvailability);
167            }
168        }
169        return $validationErrors;
170    }
171
172    private function validateAvailability(
173        Availability $availability,
174        AvailabilityList $existingAvailabilities,
175        \DateTimeImmutable $selectedDate
176    ): array {
177        $startDate = (new \DateTimeImmutable())->setTimestamp($availability->startDate);
178        $endDate = (new \DateTimeImmutable())->setTimestamp($availability->endDate);
179        $startDateTime = new \DateTimeImmutable(
180            "{$startDate->format('Y-m-d')} {$availability->startTime}"
181        );
182        $endDateTime = new \DateTimeImmutable(
183            "{$endDate->format('Y-m-d')} {$availability->endTime}"
184        );
185
186        return $existingAvailabilities->validateTimeRangesAndRules(
187            $startDateTime,
188            $endDateTime,
189            $selectedDate,
190            $availability->kind ?? 'default',
191            $availability->bookable['startInDays'],
192            $availability->bookable['endInDays'],
193            $availability->weekday
194        );
195    }
196
197    private function createAvailabilityList(array $availabilityData): AvailabilityList
198    {
199        $availabilities = new AvailabilityList();
200        foreach ($availabilityData as $data) {
201            $availability = new Availability($data);
202            $availability->testValid();
203            $availabilities->addEntity($availability);
204        }
205        return $availabilities;
206    }
207
208    private function createSelectedDateTime(string $selectedDate): \DateTimeImmutable
209    {
210        return \DateTimeImmutable::createFromFormat(
211            'Y-m-d H:i:s',
212            $selectedDate . ' 00:00:00'
213        );
214    }
215
216    private function createMergedAvailabilityList(\BO\Zmsentities\Scope $scope): AvailabilityList
217    {
218        $availabilityRepo = new AvailabilityRepository();
219        $existingAvailabilities = $availabilityRepo->readAvailabilityListByScope($scope, 1);
220
221        $mergedAvailabilities = new AvailabilityList();
222        foreach ($existingAvailabilities as $availability) {
223            $mergedAvailabilities->addEntity($availability);
224        }
225        return $mergedAvailabilities;
226    }
227
228    private static function validateClientData(array $input): void
229    {
230        if (empty($input)) {
231            App::$log->warning('No input data provided');
232            throw new BadRequestException('No input data provided');
233        }
234
235        if (!isset($input['availabilityList']) || !is_array($input['availabilityList']) || empty($input['availabilityList'])) {
236            App::$log->warning('Invalid availabilityList', [
237                'has_availabilityList' => isset($input['availabilityList']),
238                'is_array' => isset($input['availabilityList']) ? is_array($input['availabilityList']) : false,
239                'is_empty' => isset($input['availabilityList']) ? empty($input['availabilityList']) : true
240            ]);
241            throw new BadRequestException('Invalid availabilityList');
242        }
243    }
244
245    private static function validateScopeConsistency(array $availabilityList): void
246    {
247        if (empty($availabilityList)) {
248            return;
249        }
250
251        $firstScope = null;
252        foreach ($availabilityList as $index => $availability) {
253            $currentScope = $availability['scope']['id'] ?? null;
254            if (!$currentScope) {
255                App::$log->warning('Missing scope id in availability', ['index' => $index]);
256                throw new BadRequestException('Missing scope id in availability list');
257            }
258            if ($firstScope === null) {
259                $firstScope = $currentScope;
260            } elseif ($currentScope !== $firstScope) {
261                App::$log->warning('Inconsistent scopes in availability list', [
262                    'first_scope' => $firstScope,
263                    'different_scope' => $currentScope,
264                    'index' => $index
265                ]);
266                throw new BadRequestException('All availabilities must belong to the same scope');
267            }
268        }
269    }
270}