Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
93.27% |
97 / 104 |
|
33.33% |
1 / 3 |
CRAP | |
0.00% |
0 / 1 |
ImportMembersEndpoint | |
93.27% |
97 / 104 |
|
33.33% |
1 / 3 |
21.13 | |
0.00% |
0 / 1 |
handle | |
93.18% |
82 / 88 |
|
0.00% |
0 / 1 |
15.07 | |||
getCsvContent | |
87.50% |
7 / 8 |
|
0.00% |
0 / 1 |
2.01 | |||
getUserData | |
100.00% |
8 / 8 |
|
100.00% |
1 / 1 |
4 |
1 | <?php |
2 | |
3 | namespace Olz\Apps\Members\Endpoints; |
4 | |
5 | use Olz\Api\OlzTypedEndpoint; |
6 | use Olz\Apps\Members\Utils\MembersUtilsTrait; |
7 | use Olz\Entity\Members\Member; |
8 | use Olz\Entity\Users\User; |
9 | use 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 | */ |
30 | class 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 | } |