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