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