Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
94.12% covered (success)
94.12%
144 / 153
83.33% covered (warning)
83.33%
10 / 12
CRAP
0.00% covered (danger)
0.00%
0 / 1
AvailabilityListUpdate
94.12% covered (success)
94.12%
144 / 153
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%
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                    'startDate' => $availability->startDate,
116                    'endDate' => $availability->endDate,
117                    'startTime' => $availability->startTime,
118                    'endTime' => $availability->endTime,
119                    'type' => $availability->type,
120                    'operation' => 'update'
121                ]);
122            }
123        } else {
124            $updatedAvailability = $repository->writeEntity($availability, $resolveReferences);
125            App::$log->info('Created new availability', [
126                'id' => $updatedAvailability->id,
127                'scope_id' => $availability->scope['id'],
128                'startDate' => $availability->startDate,
129                'endDate' => $availability->endDate,
130                'startTime' => $availability->startTime,
131                'endTime' => $availability->endTime,
132                'type' => $availability->type,
133                'operation' => 'create'
134            ]);
135        }
136        if (!$updatedAvailability) {
137            throw new AvailabilityListUpdateFailed();
138        }
139        return $updatedAvailability;
140    }
141
142    protected function resetOpeningHours(Availability $availability): void
143    {
144        $doubleTypeAvailability = (new AvailabilityRepository())->readEntityDoubleTypes($availability->id);
145        if ($doubleTypeAvailability) {
146            $doubleTypeAvailability->workstationCount['intern'] = 0;
147            $doubleTypeAvailability->workstationCount['callcenter'] = 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}