Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 105
0.00% covered (danger)
0.00%
0 / 3
CRAP
0.00% covered (danger)
0.00%
0 / 1
ImportMembersEndpoint
0.00% covered (danger)
0.00%
0 / 105
0.00% covered (danger)
0.00%
0 / 3
462
0.00% covered (danger)
0.00%
0 / 1
 handle
0.00% covered (danger)
0.00%
0 / 89
0.00% covered (danger)
0.00%
0 / 1
240
 getCsvContent
0.00% covered (danger)
0.00%
0 / 8
0.00% covered (danger)
0.00%
0 / 1
6
 getUserData
0.00% covered (danger)
0.00%
0 / 8
0.00% covered (danger)
0.00%
0 / 1
20
1<?php
2
3namespace Olz\Apps\Members\Endpoints;
4
5use Olz\Api\OlzTypedEndpoint;
6use Olz\Apps\Members\Utils\MembersUtils;
7use Olz\Entity\Members\Member;
8use Olz\Entity\Users\User;
9use PhpTypeScriptApi\HttpError;
10
11/**
12 * @phpstan-type OlzMemberInfo array{
13 *   ident: non-empty-string,
14 *   action: 'CREATE'|'UPDATE'|'DELETE'|'KEEP',
15 *   username?: ?non-empty-string,
16 *   matchingUsername?: ?non-empty-string,
17 *   user?: ?array{
18 *     id: int,
19 *     firstName: non-empty-string,
20 *     lastName: non-empty-string,
21 *   },
22 *   updates: array<non-empty-string, array{old: string, new: string}>,
23 * }
24 *
25 * @extends OlzTypedEndpoint<
26 *   array{csvFileId: non-empty-string},
27 *   array{status: 'OK'|'ERROR', members: array<OlzMemberInfo>}
28 * >
29 */
30class ImportMembersEndpoint extends OlzTypedEndpoint {
31    protected function handle(mixed $input): mixed {
32        if (!$this->authUtils()->hasPermission('vorstand')) {
33            throw new HttpError(403, "Kein Zugriff!");
34        }
35
36        $user = $this->authUtils()->getCurrentUser();
37        $this->log()->info("Members import by {$user?->getUsername()}.");
38
39        $member_info_by_ident = [];
40        $member_utils = new MembersUtils();
41        $csv_content = $this->getCsvContent($input['csvFileId']);
42        $members = $member_utils->parseCsv($csv_content);
43        $member_repo = $this->entityManager()->getRepository(Member::class);
44        $user_repo = $this->entityManager()->getRepository(User::class);
45
46        $existing_member_is_deleted = [];
47        foreach ($member_repo->getAllIdents() as $existing_member_ident) {
48            // Assume deleted unless set to false later...
49            $existing_member_is_deleted[$existing_member_ident] = true;
50        }
51        foreach ($members as $member) {
52            $member_ident = $member_utils->getMemberIdent($member);
53            $member_username = $member_utils->getMemberUsername($member);
54            $enc_member = json_encode($member);
55            $this->generalUtils()->checkNotFalse($enc_member, "JSON encode failed");
56            if (!$member_ident) {
57                $this->log()->warning("Member has no ident: {$enc_member}");
58                continue;
59            }
60            $existing_member_is_deleted[$member_ident] = false;
61            $user = $member_username ? (
62                $user_repo->findOneBy(['username' => $member_username])
63                ?? $user_repo->findOneBy(['old_username' => $member_username])
64            ) : null;
65            $matching_user = $user_repo->findUserFuzzilyByName(
66                trim($member_utils->getMemberFirstName($member) ?? ''),
67                trim($member_utils->getMemberLastName($member) ?? ''),
68            );
69            $base_info = [
70                'username' => $member_username,
71                'matchingUsername' => $matching_user?->getUsername(),
72                'user' => $this->getUserData($user),
73            ];
74            $entity = $member_repo->findOneBy(['ident' => $member_ident]);
75            if (!$entity) {
76                $member_info_by_ident[$member_ident] = [...$base_info, 'action' => 'CREATE'];
77                $entity = new Member();
78                $this->entityUtils()->createOlzEntity($entity, ['onOff' => true]);
79                $entity->setIdent($member_ident);
80                $entity->setUser($user);
81                $entity->setData($enc_member);
82                $entity->setUpdates(null);
83                $member_utils->update($entity, $user);
84                $this->entityManager()->persist($entity);
85            } else {
86                if ($entity->getData() === $enc_member && $entity->getUser() === $user) {
87                    $member_info_by_ident[$member_ident] = [...$base_info, 'action' => 'KEEP'];
88                    $member_utils->update($entity, $user);
89                } else {
90                    $member_info_by_ident[$member_ident] = [...$base_info, 'action' => 'UPDATE'];
91                    $this->entityUtils()->updateOlzEntity($entity, []);
92                    $entity->setUser($user);
93                    $entity->setData($enc_member);
94                    $member_utils->update($entity, $user);
95                }
96            }
97            $new_value_by_key = json_decode($entity->getUpdates() ?? '[]', true) ?: [];
98            $updates = [];
99            foreach ($new_value_by_key as $key => $new_value) {
100                $updates[$key] = ['old' => $member[$key] ?? '', 'new' => $new_value];
101            }
102            $member_info_by_ident[$member_ident]['updates'] = $updates;
103        }
104        foreach ($existing_member_is_deleted as $int_ident => $is_deleted) {
105            $member_ident = "{$int_ident}";
106            if ($is_deleted) {
107                $member = $member_repo->findOneBy(['ident' => $member_ident]);
108                $member_info_by_ident[$member_ident] = [
109                    'action' => 'DELETE',
110                    'username' => null,
111                    'matchingUsername' => null,
112                    'user' => $this->getUserData($member?->getUser()),
113                    'updates' => [],
114                ];
115                if ($member) {
116                    $this->entityManager()->remove($member);
117                } else {
118                    $this->log()->warning("Cannot delete inexistent member: {$member_ident}");
119                }
120            }
121        }
122        $this->entityManager()->flush();
123
124        $members = [];
125        foreach ($member_info_by_ident as $int_ident => $member) {
126            $member_ident = "{$int_ident}";
127            $this->generalUtils()->checkNotEmpty($member_ident, 'Member ident must not be empty');
128            $this->generalUtils()->checkNotEmpty($member['username'], 'Member username must not be empty');
129            $this->generalUtils()->checkNotEmpty($member['matchingUsername'], 'Member matchingUsername must not be empty');
130            $members[] = [
131                'ident' => "{$member_ident}",
132                'action' => $member['action'],
133                'username' => $member['username'] ?? null,
134                'matchingUsername' => $member['matchingUsername'] ?? null,
135                'user' => $member['user'] ?? null,
136                'updates' => $member['updates'],
137            ];
138        }
139        return ['status' => 'OK', 'members' => $members];
140    }
141
142    protected function getCsvContent(string $upload_id): string {
143        $data_path = $this->envUtils()->getDataPath();
144        $upload_path = "{$data_path}temp/{$upload_id}";
145        if (!is_file($upload_path)) {
146            throw new HttpError(400, 'Uploaded file not found!');
147        }
148        $csv_content = file_get_contents($upload_path);
149        unlink($upload_path);
150        $this->generalUtils()->checkNotFalse($csv_content, "Could not read uploaded Members CSV");
151        return $csv_content;
152    }
153
154    /** @return ?array{
155     *     id: int,
156     *     firstName: non-empty-string,
157     *     lastName: non-empty-string,
158     *   }
159     */
160    protected function getUserData(?User $user): ?array {
161        $user_id = $user?->getId();
162        if (!$user_id) {
163            return null;
164        }
165        return [
166            'id' => $user_id,
167            'firstName' => $user->getFirstName() ?: '-',
168            'lastName' => $user->getLastName() ?: '-',
169        ];
170    }
171}