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