Issues (2)

src/Entity/Event.php (2 issues)

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
The expression return $this->dataBag->g...participants', array()) could return the type null which is incompatible with the type-hinted return array. Consider adding an additional type-check to rule them out.
Loading history...
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
Bug Best Practice introduced by
The expression return new DateTimeImmut...eZone($this->timezone)) could return the type false which is incompatible with the type-hinted return DateTimeInterface|null. Consider adding an additional type-check to rule them out.
Loading history...
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