Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
93.62% covered (success)
93.62%
132 / 141
83.33% covered (warning)
83.33%
10 / 12
CRAP
0.00% covered (danger)
0.00%
0 / 1
AvailabilityListUpdate
93.62% covered (success)
93.62%
132 / 141
83.33% covered (warning)
83.33%
10 / 12
34.30
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%
21 / 21
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                $this->resetOpeningHours($existingAvailability);
109                $updatedAvailability = $repository->updateEntity($availability->id, $availability, $resolveReferences);
110                App::$log->info('Updated availability', [
111                    'id' => $availability->id,
112                    'scope_id' => $availability->scope['id'],
113                    'operation' => 'update'
114                ]);
115            }
116        } else {
117            $updatedAvailability = $repository->writeEntity($availability, $resolveReferences);
118            App::$log->info('Created new availability', [
119                'id' => $updatedAvailability->id,
120                'scope_id' => $availability->scope['id'],
121                'operation' => 'create'
122            ]);
123        }
124        if (!$updatedAvailability) {
125            throw new AvailabilityListUpdateFailed();
126        }
127        return $updatedAvailability;
128    }
129
130    protected function resetOpeningHours(Availability $availability): void
131    {
132        $doubleTypeAvailability = (new AvailabilityRepository())->readEntityDoubleTypes($availability->id);
133        if ($doubleTypeAvailability) {
134            $doubleTypeAvailability->workstationCount['intern'] = 0;
135            $doubleTypeAvailability->workstationCount['callcenter'] = 0;
136            $doubleTypeAvailability->workstationCount['public'] = 0;
137            $doubleTypeAvailability['description'] = '';
138            $doubleTypeAvailability['type'] = 'openinghours';
139            (new AvailabilityRepository())->writeEntity($doubleTypeAvailability);
140        }
141    }
142
143    private function validateAvailabilityList(
144        AvailabilityList $newAvailabilities,
145        AvailabilityList $existingAvailabilities,
146        \DateTimeImmutable $selectedDate
147    ): array {
148        $validationErrors = [];
149        foreach ($newAvailabilities as $newAvailability) {
150            $errors = $this->validateAvailability($newAvailability, $existingAvailabilities, $selectedDate);
151            if (count($errors) > 0) {
152                $validationErrors = array_merge($validationErrors, $errors);
153            } else {
154                $existingAvailabilities->addEntity($newAvailability);
155            }
156        }
157        return $validationErrors;
158    }
159
160    private function validateAvailability(
161        Availability $availability,
162        AvailabilityList $existingAvailabilities,
163        \DateTimeImmutable $selectedDate
164    ): array {
165        $startDate = (new \DateTimeImmutable())->setTimestamp($availability->startDate);
166        $endDate = (new \DateTimeImmutable())->setTimestamp($availability->endDate);
167        $startDateTime = new \DateTimeImmutable(
168            "{$startDate->format('Y-m-d')} {$availability->startTime}"
169        );
170        $endDateTime = new \DateTimeImmutable(
171            "{$endDate->format('Y-m-d')} {$availability->endTime}"
172        );
173
174        return $existingAvailabilities->validateTimeRangesAndRules(
175            $startDateTime,
176            $endDateTime,
177            $selectedDate,
178            $availability->kind ?? 'default',
179            $availability->bookable['startInDays'],
180            $availability->bookable['endInDays'],
181            $availability->weekday
182        );
183    }
184
185    private function createAvailabilityList(array $availabilityData): AvailabilityList
186    {
187        $availabilities = new AvailabilityList();
188        foreach ($availabilityData as $data) {
189            $availability = new Availability($data);
190            $availability->testValid();
191            $availabilities->addEntity($availability);
192        }
193        return $availabilities;
194    }
195
196    private function createSelectedDateTime(string $selectedDate): \DateTimeImmutable
197    {
198        return \DateTimeImmutable::createFromFormat(
199            'Y-m-d H:i:s',
200            $selectedDate . ' 00:00:00'
201        );
202    }
203
204    private function createMergedAvailabilityList(\BO\Zmsentities\Scope $scope): AvailabilityList
205    {
206        $availabilityRepo = new AvailabilityRepository();
207        $existingAvailabilities = $availabilityRepo->readAvailabilityListByScope($scope, 1);
208
209        $mergedAvailabilities = new AvailabilityList();
210        foreach ($existingAvailabilities as $availability) {
211            $mergedAvailabilities->addEntity($availability);
212        }
213        return $mergedAvailabilities;
214    }
215
216    private static function validateClientData(array $input): void
217    {
218        if (empty($input)) {
219            App::$log->warning('No input data provided');
220            throw new BadRequestException('No input data provided');
221        }
222
223        if (!isset($input['availabilityList']) || !is_array($input['availabilityList']) || empty($input['availabilityList'])) {
224            App::$log->warning('Invalid availabilityList', [
225                'has_availabilityList' => isset($input['availabilityList']),
226                'is_array' => isset($input['availabilityList']) ? is_array($input['availabilityList']) : false,
227                'is_empty' => isset($input['availabilityList']) ? empty($input['availabilityList']) : true
228            ]);
229            throw new BadRequestException('Invalid availabilityList');
230        }
231    }
232
233    private static function validateScopeConsistency(array $availabilityList): void
234    {
235        if (empty($availabilityList)) {
236            return;
237        }
238
239        $firstScope = null;
240        foreach ($availabilityList as $index => $availability) {
241            $currentScope = $availability['scope']['id'] ?? null;
242            if (!$currentScope) {
243                App::$log->warning('Missing scope id in availability', ['index' => $index]);
244                throw new BadRequestException('Missing scope id in availability list');
245            }
246            if ($firstScope === null) {
247                $firstScope = $currentScope;
248            } elseif ($currentScope !== $firstScope) {
249                App::$log->warning('Inconsistent scopes in availability list', [
250                    'first_scope' => $firstScope,
251                    'different_scope' => $currentScope,
252                    'index' => $index
253                ]);
254                throw new BadRequestException('All availabilities must belong to the same scope');
255            }
256        }
257    }
258}