1
|
|
|
<?php |
2
|
|
|
|
3
|
|
|
/* |
4
|
|
|
* This file is part of the vseth-semesterly-reports project. |
5
|
|
|
* |
6
|
|
|
* (c) Florian Moser <[email protected]> |
7
|
|
|
* |
8
|
|
|
* For the full copyright and license information, please view the LICENSE |
9
|
|
|
* file that was distributed with this source code. |
10
|
|
|
*/ |
11
|
|
|
|
12
|
|
|
namespace App\Service; |
13
|
|
|
|
14
|
|
|
use App\Entity\Reservation; |
15
|
|
|
use App\Entity\User; |
16
|
|
|
use App\Helper\DateTimeHelper; |
17
|
|
|
use App\Model\ImportStatistics; |
18
|
|
|
use App\Service\Interfaces\BillServiceInterface; |
19
|
|
|
use App\Service\Interfaces\ImportServiceInterface; |
20
|
|
|
use Doctrine\Persistence\ManagerRegistry; |
21
|
|
|
use Symfony\Component\DependencyInjection\ParameterBag\ParameterBagInterface; |
22
|
|
|
use Symfony\Component\HttpFoundation\File\UploadedFile; |
23
|
|
|
|
24
|
|
|
class ImportService implements ImportServiceInterface |
25
|
|
|
{ |
26
|
|
|
/** |
27
|
|
|
* @var string |
28
|
|
|
*/ |
29
|
|
|
private $basePath; |
30
|
|
|
|
31
|
|
|
/** @var ManagerRegistry */ |
32
|
|
|
private $doctrine; |
33
|
|
|
|
34
|
|
|
/** @var BillServiceInterface */ |
35
|
|
|
private $billService; |
36
|
|
|
|
37
|
|
|
const DELIMITER = ','; |
38
|
|
|
|
39
|
|
|
/** |
40
|
|
|
* ImportService constructor. |
41
|
|
|
* |
42
|
|
|
* @param BillServiceInterface $billService |
43
|
|
|
*/ |
44
|
|
|
public function __construct(ParameterBagInterface $parameterBag, ManagerRegistry $doctrine, Interfaces\BillServiceInterface $billService) |
45
|
|
|
{ |
46
|
|
|
$this->basePath = $parameterBag->get('UPLOAD_DIR'); |
47
|
|
|
$this->doctrine = $doctrine; |
48
|
|
|
$this->billService = $billService; |
49
|
|
|
} |
50
|
|
|
|
51
|
|
|
/** |
52
|
|
|
* {@inheritdoc} |
53
|
|
|
* |
54
|
|
|
* @throws \Exception |
55
|
|
|
*/ |
56
|
|
|
public function import(UploadedFile $users, UploadedFile $reservations, \DateTime $periodStart, \DateTime $periodEnd) |
57
|
|
|
{ |
58
|
|
|
$reservationsPath = $this->uploadFile($reservations, 'reservations.csv'); |
59
|
|
|
$usersPath = $this->uploadFile($users, 'users.csv'); |
60
|
|
|
|
61
|
|
|
$periodStartString = $periodStart->format('Y-m-d'); |
62
|
|
|
$periodEndString = $periodEnd->format('Y-m-d'); |
63
|
|
|
|
64
|
|
|
/** @var \DateTime[] $lastSubscriptionEndByUser */ |
65
|
|
|
$lastSubscriptionEndByUser = []; |
66
|
|
|
/** @var Reservation[][] $reservationsByUser */ |
67
|
|
|
$reservationsByUser = []; |
68
|
|
|
$this->parseReservations($reservationsPath, $periodStartString, $periodEndString, $lastSubscriptionEndByUser, $reservationsByUser); |
69
|
|
|
|
70
|
|
|
/** @var User[] $users */ |
71
|
|
|
$users = []; |
72
|
|
|
$this->parseUsers($usersPath, $reservationsByUser, $lastSubscriptionEndByUser, $users); |
73
|
|
|
|
74
|
|
|
$this->calculateAmountOwed($users); |
75
|
|
|
|
76
|
|
|
$this->saveAll($users, $reservationsByUser); |
77
|
|
|
|
78
|
|
|
$totalReservations = 0; |
79
|
|
|
foreach ($reservationsByUser as $item) { |
80
|
|
|
$totalReservations += \count($item); |
81
|
|
|
} |
82
|
|
|
|
83
|
|
|
$totalAmountOwed = 0; |
84
|
|
|
foreach ($users as $user) { |
85
|
|
|
$totalAmountOwed += $user->getAmountOwed(); |
86
|
|
|
} |
87
|
|
|
|
88
|
|
|
return new ImportStatistics(\count($users), $totalReservations, $totalAmountOwed); |
89
|
|
|
} |
90
|
|
|
|
91
|
|
|
/** |
92
|
|
|
* @param User[] $users |
93
|
|
|
*/ |
94
|
|
|
private function calculateAmountOwed(array $users) |
95
|
|
|
{ |
96
|
|
|
foreach ($users as $user) { |
97
|
|
|
$amountOwed = $this->billService->getAmountOwed($user); |
98
|
|
|
$user->setAmountOwed($amountOwed); |
99
|
|
|
$user->setAmountOwedWithFees($amountOwed); |
100
|
|
|
} |
101
|
|
|
} |
102
|
|
|
|
103
|
|
|
/** |
104
|
|
|
* @throws \Exception |
105
|
|
|
*/ |
106
|
|
|
private function parseReservations(string $reservationsPath, string $periodStart, string $periodEnd, array &$lastSubscriptionEndByUser, array &$reservationsByUser) |
107
|
|
|
{ |
108
|
|
|
$row = 1; |
109
|
|
|
if (($handle = fopen($reservationsPath, 'r')) !== false) { |
110
|
|
|
while (($data = fgetcsv($handle, 1000, self::DELIMITER)) !== false) { |
111
|
|
|
if ($row++ === 1) { |
112
|
|
|
$this->ensureReservationHeader($data); |
113
|
|
|
} |
114
|
|
|
|
115
|
|
|
$num = \count($data); |
116
|
|
|
if ($num < 9) { |
117
|
|
|
continue; |
118
|
|
|
} |
119
|
|
|
|
120
|
|
|
$user = $data[5]; |
121
|
|
|
$start = $data[7]; |
122
|
|
|
if ($start < $periodStart) { |
123
|
|
|
$periodDate = mb_substr($data[7], 0, 10); // 2019-01-01 |
124
|
|
|
if (isset($lastSubscriptionEndByUser[$user]) && $lastSubscriptionEndByUser[$user] > $periodDate) { |
125
|
|
|
continue; |
126
|
|
|
} |
127
|
|
|
|
128
|
|
|
// remember new subscription |
129
|
|
|
$newSubscriptiondEnd = DateTimeHelper::getSubscriptionEnd(new \DateTime($periodDate)); |
130
|
|
|
$lastSubscriptionEndByUser[$user] = $newSubscriptiondEnd->format('Y-m-d'); |
131
|
|
|
|
132
|
|
|
continue; |
133
|
|
|
} |
134
|
|
|
|
135
|
|
|
if ($start >= $periodStart && $start <= $periodEnd) { |
136
|
|
|
$reservation = new Reservation(); |
137
|
|
|
$reservation->setStart(new \DateTime($start)); |
138
|
|
|
$reservation->setEnd(new \DateTime($data[8])); |
139
|
|
|
|
140
|
|
|
if ($reservation->getEnd()->format('i') === 59) { // add one minute if datetime ends in :59 |
141
|
|
|
$reservation->getEnd()->add(new \DateInterval('PT1M')); |
142
|
|
|
} |
143
|
|
|
|
144
|
|
|
// abort if other weird date failures |
145
|
|
|
if ($reservation->getEnd()->format('i') !== 0 || $reservation->getStart()->format('i') !== 0) { |
146
|
|
|
throw new \Exception('can not handle non-full hour start/end times'); |
147
|
|
|
} |
148
|
|
|
|
149
|
|
|
$reservation->setRoom($data[6]); |
150
|
|
|
|
151
|
|
|
$reservation->setModifiedAt(new \DateTime($data[2])); |
152
|
|
|
$reservation->setCreatedAt(new \DateTime($data[3])); |
153
|
|
|
|
154
|
|
|
$reservationsByUser[$user][] = $reservation; |
155
|
|
|
|
156
|
|
|
return; |
157
|
|
|
|
158
|
|
|
continue; |
|
|
|
|
159
|
|
|
} |
160
|
|
|
} |
161
|
|
|
fclose($handle); |
162
|
|
|
} |
163
|
|
|
} |
164
|
|
|
|
165
|
|
|
/** |
166
|
|
|
* @param Reservation[][] $reservationsByUser |
167
|
|
|
* @param \DateTime[] $lastSubscriptionEndByUser |
168
|
|
|
* @param User[] $users |
169
|
|
|
* |
170
|
|
|
* @throws \Exception |
171
|
|
|
*/ |
172
|
|
|
private function parseUsers(string $usersPath, array &$reservationsByUser, array $lastSubscriptionEndByUser, array &$users) |
173
|
|
|
{ |
174
|
|
|
$row = 1; |
175
|
|
|
if (($handle = fopen($usersPath, 'r')) !== false) { |
176
|
|
|
while (($data = fgetcsv($handle, 1000, self::DELIMITER)) !== false) { |
177
|
|
|
if ($row++ === 1) { |
178
|
|
|
$this->ensureUsersHeader($data); |
179
|
|
|
} |
180
|
|
|
|
181
|
|
|
if (\count($data) < 14) { |
182
|
|
|
continue; |
183
|
|
|
} |
184
|
|
|
|
185
|
|
|
$userId = $data[0]; |
186
|
|
|
if (!isset($reservationsByUser[$userId]) || \count($reservationsByUser[$userId]) === 0) { |
187
|
|
|
continue; |
188
|
|
|
} |
189
|
|
|
|
190
|
|
|
$user = new User(); |
191
|
|
|
$user->setGivenName($data[8]); |
192
|
|
|
$user->setFamilyName($data[9]); |
193
|
|
|
$user->setAddress($data[10]); |
194
|
|
|
$user->setEmail($data[11]); |
195
|
|
|
$user->setPhone($data[12]); |
196
|
|
|
$user->setCategory($data[14]); |
197
|
|
|
|
198
|
|
|
$subscriptionEnd = isset($lastSubscriptionEndByUser[$userId]) ? new \DateTime($lastSubscriptionEndByUser[$userId]) : null; |
|
|
|
|
199
|
|
|
$user->setLastPayedPeriodicFeeEnd($subscriptionEnd); |
200
|
|
|
|
201
|
|
|
foreach ($reservationsByUser[$userId] as $reservation) { |
202
|
|
|
$reservation->setUser($user); |
203
|
|
|
$user->getReservations()->add($reservation); |
204
|
|
|
} |
205
|
|
|
|
206
|
|
|
$users[] = $user; |
207
|
|
|
} |
208
|
|
|
} |
209
|
|
|
} |
210
|
|
|
|
211
|
|
|
private function ensureReservationHeader(array $data) |
212
|
|
|
{ |
213
|
|
|
$expectedSize = 10; |
214
|
|
|
if (\count($data) !== $expectedSize) { |
215
|
|
|
throw new \Exception('reservation header wrong count'); |
216
|
|
|
} |
217
|
|
|
|
218
|
|
|
$expectedContent = ['id', 'archived', 'moddate', 'createdate', 'position', 'user', 'raum', 'start', 'ende', 'apiid']; |
219
|
|
|
for ($i = 0; $i < $expectedSize; ++$i) { |
220
|
|
|
if ($data[$i] !== $expectedContent[$i]) { |
221
|
|
|
throw new \Exception('the header fields must be ' . implode('', $expectedContent) . ' in that order.'); |
222
|
|
|
} |
223
|
|
|
} |
224
|
|
|
} |
225
|
|
|
|
226
|
|
|
private function ensureUsersHeader(array $data) |
227
|
|
|
{ |
228
|
|
|
$expectedSize = 20; |
229
|
|
|
if (\count($data) !== $expectedSize) { |
230
|
|
|
throw new \Exception('reservation header wrong count'); |
231
|
|
|
} |
232
|
|
|
|
233
|
|
|
$expectedContent = ['id', 'archived', 'moddate', 'createdate', 'passwort', 'seitenpagemounts', 'seitenentrytypes', 'anrede', 'vorname', 'nachname', 'adresse', 'email', 'tel', 'admin', 'category', 'passwordhash', 'badge', 'birthday', 'benutzername', 'pwchanged']; |
234
|
|
|
for ($i = 0; $i < $expectedSize; ++$i) { |
235
|
|
|
if ($data[$i] !== $expectedContent[$i]) { |
236
|
|
|
throw new \Exception('the header fields must be ' . implode('', $expectedContent) . ' in that order.'); |
237
|
|
|
} |
238
|
|
|
} |
239
|
|
|
} |
240
|
|
|
|
241
|
|
|
private function uploadFile(UploadedFile $file, string $fileName) |
242
|
|
|
{ |
243
|
|
|
$file->move($this->basePath, $fileName); |
244
|
|
|
|
245
|
|
|
return $this->basePath . \DIRECTORY_SEPARATOR . $fileName; |
246
|
|
|
} |
247
|
|
|
|
248
|
|
|
/** |
249
|
|
|
* @param User[] $users |
250
|
|
|
* @param Reservation[][] $reservationsByUser |
251
|
|
|
*/ |
252
|
|
|
private function saveAll(array $users, array $reservationsByUser) |
253
|
|
|
{ |
254
|
|
|
$manager = $this->doctrine->getManager(); |
255
|
|
|
|
256
|
|
|
foreach ($users as $user) { |
257
|
|
|
$user->generateAuthenticationCode(); |
258
|
|
|
$manager->persist($user); |
259
|
|
|
} |
260
|
|
|
|
261
|
|
|
foreach ($reservationsByUser as $reservationsBySingleUser) { |
262
|
|
|
foreach ($reservationsBySingleUser as $reservation) { |
263
|
|
|
$manager->persist($reservation); |
264
|
|
|
} |
265
|
|
|
} |
266
|
|
|
|
267
|
|
|
$manager->flush(); |
268
|
|
|
} |
269
|
|
|
} |
270
|
|
|
|
This check looks for unreachable code. It uses sophisticated control flow analysis techniques to find statements which will never be executed.
Unreachable code is most often the result of
return
,die
orexit
statements that have been added for debug purposes.In the above example, the last
return false
will never be executed, because a return statement has already been met in every possible execution path.