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