1 | <?php |
||
2 | |||
3 | declare(strict_types=1); |
||
4 | |||
5 | namespace Communibase\Entity; |
||
6 | |||
7 | use Communibase\CommunibaseId; |
||
8 | use Communibase\CommunibaseIdCollection; |
||
9 | use Communibase\DataBag; |
||
10 | use Communibase\Exception\ActionNotAllowedByDateException; |
||
11 | use Communibase\Exception\ActionNotAllowedException; |
||
12 | use Communibase\Exception\AlreadyRegisteredException; |
||
13 | use Communibase\Exception\FullyBookedException; |
||
14 | use Communibase\Exception\InvalidDateException; |
||
15 | |||
16 | /** |
||
17 | * @author Kingsquare ([email protected]) |
||
18 | * @copyright Copyright (c) Kingsquare BV (http://www.kingsquare.nl) |
||
19 | */ |
||
20 | class Event |
||
21 | { |
||
22 | public const STATUS_READY = 'Ready'; |
||
23 | |||
24 | public const PARTICIPANT_STATUS_REGISTERED = 'registered'; |
||
25 | public const PARTICIPANT_STATUS_CANCELLED = 'cancelled'; |
||
26 | |||
27 | /** |
||
28 | * @var DataBag |
||
29 | */ |
||
30 | protected $dataBag; |
||
31 | |||
32 | /** |
||
33 | * @var string |
||
34 | */ |
||
35 | protected $entityType = 'event'; |
||
36 | |||
37 | /** |
||
38 | * @var string |
||
39 | */ |
||
40 | protected $timezone = 'Europe/Amsterdam'; |
||
41 | |||
42 | /** |
||
43 | * @var string[] |
||
44 | */ |
||
45 | protected $registeredStatuses = [ |
||
46 | self::PARTICIPANT_STATUS_REGISTERED |
||
47 | ]; |
||
48 | |||
49 | /** |
||
50 | * @param array<string,mixed> $eventData |
||
51 | */ |
||
52 | final private function __construct(array $eventData) |
||
53 | { |
||
54 | $this->dataBag = DataBag::fromEntityData($this->entityType, $eventData); |
||
55 | } |
||
56 | |||
57 | /** |
||
58 | * @param array<string,mixed> $eventData |
||
59 | * @return static |
||
60 | */ |
||
61 | public static function factory(array $eventData) |
||
62 | { |
||
63 | return new static($eventData); |
||
64 | } |
||
65 | |||
66 | public function getId(): CommunibaseId |
||
67 | { |
||
68 | return CommunibaseId::fromValidString($this->dataBag->get($this->entityType . '._id')); |
||
69 | } |
||
70 | |||
71 | public function isReady(): bool |
||
72 | { |
||
73 | return $this->dataBag->get($this->entityType . '.status') === self::STATUS_READY; |
||
74 | } |
||
75 | |||
76 | public function getMaxParticipants(): ?int |
||
77 | { |
||
78 | $maxParticipants = $this->dataBag->get($this->entityType . '.maxParticipants'); |
||
79 | return $maxParticipants === null ? null : (int)$maxParticipants; |
||
80 | } |
||
81 | |||
82 | public function isFullyBooked(): bool |
||
83 | { |
||
84 | if ($this->getMaxParticipants() === null) { |
||
85 | return false; |
||
86 | } |
||
87 | return $this->getRegisteredParticipantsPersonIds()->count() >= $this->getMaxParticipants(); |
||
88 | } |
||
89 | |||
90 | public function getRegisteredParticipantsPersonIds(): CommunibaseIdCollection |
||
91 | { |
||
92 | return CommunibaseIdCollection::fromValidStrings( |
||
93 | \array_reduce( |
||
94 | $this->getParticipantsData(), |
||
95 | function (array $ids, array $participantData) { |
||
96 | if (\in_array($participantData['status'], $this->registeredStatuses, true)) { |
||
97 | $ids[] = $participantData['personId']; |
||
98 | } |
||
99 | return $ids; |
||
100 | }, |
||
101 | [] |
||
102 | ) |
||
103 | ); |
||
104 | } |
||
105 | |||
106 | /** |
||
107 | * @throws InvalidDateException |
||
108 | */ |
||
109 | public function getStartDate(): ?\DateTimeInterface |
||
110 | { |
||
111 | return $this->toDateTimeImmutable($this->dataBag->get($this->entityType . '.startDate')); |
||
112 | } |
||
113 | |||
114 | /** |
||
115 | * @throws InvalidDateException |
||
116 | */ |
||
117 | public function getRegistrationStartDate(): ?\DateTimeInterface |
||
118 | { |
||
119 | return $this->toDateTimeImmutable($this->dataBag->get($this->entityType . '.registrationStartDate')); |
||
120 | } |
||
121 | |||
122 | /** |
||
123 | * @throws InvalidDateException |
||
124 | */ |
||
125 | public function getRegistrationEndDate(): ?\DateTimeInterface |
||
126 | { |
||
127 | return $this->toDateTimeImmutable($this->dataBag->get($this->entityType . '.registrationEndDate')); |
||
128 | } |
||
129 | |||
130 | /** |
||
131 | * @throws AlreadyRegisteredException |
||
132 | * @throws FullyBookedException |
||
133 | * @throws ActionNotAllowedException |
||
134 | */ |
||
135 | public function registerParticipant(ParticipantInterface $participant, CommunibaseId $debtorId = null): void |
||
136 | { |
||
137 | $this->guardAgainstAlreadyStarted(); |
||
138 | $this->guardAgainstEventNotReady(); |
||
139 | $this->guardAgainstRegistrationClosedByDate(); |
||
140 | $this->guardAgainstAlreadyRegistered($participant); |
||
141 | $this->guardAgainstFullyBooked(); |
||
142 | |||
143 | $participantsData = $this->getParticipantsData(); |
||
144 | foreach ($participantsData as &$participantData) { |
||
145 | if ($participantData['personId'] === $participant->getId()->toString()) { |
||
146 | $participantData['status'] = self::PARTICIPANT_STATUS_REGISTERED; |
||
147 | if ($debtorId !== null) { |
||
148 | $participantData['debtorId'] = $debtorId->toString(); |
||
149 | } |
||
150 | $this->setParticipantsData($participantsData); |
||
151 | return; |
||
152 | } |
||
153 | } |
||
154 | unset($participantData); |
||
155 | $participantsData[] = [ |
||
156 | 'personId' => $participant->getId()->toString(), |
||
157 | 'status' => self::PARTICIPANT_STATUS_REGISTERED, |
||
158 | 'debtorId' => $debtorId === null ? null : $debtorId->toString() |
||
159 | ]; |
||
160 | $this->setParticipantsData($participantsData); |
||
161 | } |
||
162 | |||
163 | /** |
||
164 | * @throws ActionNotAllowedByDateException |
||
165 | */ |
||
166 | public function unRegisterParticipant(ParticipantInterface $participant): void |
||
167 | { |
||
168 | $this->guardAgainstAlreadyStarted(); |
||
169 | if (!$this->isRegisteredParticipant($participant)) { |
||
170 | return; |
||
171 | } |
||
172 | $participantsData = $this->getParticipantsData(); |
||
173 | foreach ($participantsData as &$participantData) { |
||
174 | if ($participantData['personId'] === $participant->getId()->toString()) { |
||
175 | $participantData['status'] = self::PARTICIPANT_STATUS_CANCELLED; |
||
176 | } |
||
177 | } |
||
178 | unset($participantData); |
||
179 | $this->setParticipantsData($participantsData); |
||
180 | } |
||
181 | |||
182 | public function isRegisteredParticipant(ParticipantInterface $participant): bool |
||
183 | { |
||
184 | foreach ($this->getParticipantsData() as $participantData) { |
||
185 | if (\in_array($participantData['status'], $this->registeredStatuses, true) |
||
186 | && $participantData['personId'] === $participant->getId()->toString()) { |
||
187 | return true; |
||
188 | } |
||
189 | } |
||
190 | return false; |
||
191 | } |
||
192 | |||
193 | /** |
||
194 | * @return array<array<string,mixed>> |
||
195 | */ |
||
196 | private function getParticipantsData(): array |
||
197 | { |
||
198 | return $this->dataBag->get($this->entityType . '.participants', []); |
||
0 ignored issues
–
show
Bug
Best Practice
introduced
by
![]() |
|||
199 | } |
||
200 | |||
201 | /** |
||
202 | * @param array<string,array<string,mixed>> $participantsData |
||
203 | */ |
||
204 | private function setParticipantsData(array $participantsData): void |
||
205 | { |
||
206 | $this->dataBag->set($this->entityType . '.participants', $participantsData); |
||
207 | } |
||
208 | |||
209 | /** |
||
210 | * Convert CB dateTime to local timezone |
||
211 | * @throws InvalidDateException |
||
212 | */ |
||
213 | private function toDateTimeImmutable(?string $string): ?\DateTimeInterface |
||
214 | { |
||
215 | if (empty($string)) { |
||
216 | return null; |
||
217 | } |
||
218 | try { |
||
219 | return (new \DateTimeImmutable($string))->setTimezone(new \DateTimeZone($this->timezone)); |
||
0 ignored issues
–
show
|
|||
220 | } catch (\Exception $e) { |
||
221 | throw new InvalidDateException('Invalid date found: ' . $string); |
||
222 | } |
||
223 | } |
||
224 | |||
225 | /** |
||
226 | * @throws ActionNotAllowedByDateException |
||
227 | */ |
||
228 | private function guardAgainstAlreadyStarted(): void |
||
229 | { |
||
230 | try { |
||
231 | if ($this->getStartDate() === null) { |
||
232 | return; |
||
233 | } |
||
234 | if (new \DateTimeImmutable() < $this->getStartDate()) { |
||
235 | return; |
||
236 | } |
||
237 | } catch (\Exception $e) { |
||
238 | // throws below |
||
239 | } |
||
240 | throw new ActionNotAllowedByDateException('Event is already started.'); |
||
241 | } |
||
242 | |||
243 | /** |
||
244 | * @throws FullyBookedException |
||
245 | */ |
||
246 | private function guardAgainstFullyBooked(): void |
||
247 | { |
||
248 | if ($this->isFullyBooked()) { |
||
249 | throw new FullyBookedException('Event is fully booked.'); |
||
250 | } |
||
251 | } |
||
252 | |||
253 | /** |
||
254 | * @throws AlreadyRegisteredException |
||
255 | */ |
||
256 | private function guardAgainstAlreadyRegistered(ParticipantInterface $participant): void |
||
257 | { |
||
258 | if ($this->isRegisteredParticipant($participant)) { |
||
259 | throw new AlreadyRegisteredException('Participant is already registered.'); |
||
260 | } |
||
261 | } |
||
262 | |||
263 | /** |
||
264 | * @throws ActionNotAllowedException |
||
265 | */ |
||
266 | private function guardAgainstEventNotReady(): void |
||
267 | { |
||
268 | if (!$this->isReady()) { |
||
269 | throw new ActionNotAllowedException('Event status is not ready.'); |
||
270 | } |
||
271 | } |
||
272 | |||
273 | /** |
||
274 | * @throws ActionNotAllowedByDateException |
||
275 | */ |
||
276 | private function guardAgainstRegistrationClosedByDate(): void |
||
277 | { |
||
278 | try { |
||
279 | $registrationStartDate = $this->getRegistrationStartDate(); |
||
280 | if ($registrationStartDate !== null && (new \DateTimeImmutable() < $registrationStartDate)) { |
||
281 | throw new ActionNotAllowedByDateException( |
||
282 | 'Registration starts on ' . $registrationStartDate->format('r') |
||
283 | ); |
||
284 | } |
||
285 | $registrationEndDate = $this->getRegistrationEndDate(); |
||
286 | if ($registrationEndDate !== null && (new \DateTimeImmutable() > $registrationEndDate)) { |
||
287 | throw new ActionNotAllowedByDateException( |
||
288 | 'Registration ended on ' . $registrationEndDate->format('r') |
||
289 | ); |
||
290 | } |
||
291 | } catch (InvalidDateException $e) { |
||
292 | throw new ActionNotAllowedByDateException('Invalid date found.'); |
||
293 | } |
||
294 | } |
||
295 | } |
||
296 |