Failed Conditions
Push — master ( 3358c7...2378df )
by Sam
04:40
created

Bookable::getStatus()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 2
CRAP Score 1

Importance

Changes 0
Metric Value
eloc 1
c 0
b 0
f 0
dl 0
loc 3
rs 10
ccs 2
cts 2
cp 1
cc 1
nc 1
nop 0
crap 1
1
<?php
2
3
declare(strict_types=1);
4
5
namespace Application\Model;
6
7
use Application\Api\Input\Operator\BookableBookingCount\BookableBookingCountEqualOperatorType;
8
use Application\Api\Input\Operator\BookableBookingCount\BookableBookingCountGreaterOperatorType;
9
use Application\Api\Input\Operator\BookableBookingCount\BookableBookingCountGreaterOrEqualOperatorType;
10
use Application\Api\Input\Operator\BookableBookingCount\BookableBookingCountLessOperatorType;
11
use Application\Api\Input\Operator\BookableBookingCount\BookableBookingCountLessOrEqualOperatorType;
12
use Application\Api\Input\Operator\BookableUsageOperatorType;
13
use Application\Enum\BookableState;
14
use Application\Enum\BookableStatus;
15
use Application\Enum\BookingStatus;
16
use Application\Enum\BookingType;
17
use Application\Repository\BookableRepository;
18
use Application\Repository\BookableTagRepository;
19
use Application\Traits\HasCode;
20
use Application\Traits\HasRemarks;
21
use Cake\Chronos\ChronosDate;
22
use Doctrine\Common\Collections\ArrayCollection;
23
use Doctrine\Common\Collections\Collection;
24
use Doctrine\ORM\Mapping as ORM;
25
use Ecodev\Felix\Model\Traits\HasDescription;
26
use Ecodev\Felix\Model\Traits\HasName;
27
use GraphQL\Doctrine\Attribute as API;
28
use Money\Money;
29
30
/**
31
 * An item that can be booked by a user.
32
 */
33
#[API\Filter(field: 'custom', operator: BookableUsageOperatorType::class, type: 'id')]
34
#[API\Filter(field: 'bookableBookingCount', operator: BookableBookingCountEqualOperatorType::class, type: 'int')]
35
#[API\Filter(field: 'bookableBookingCount', operator: BookableBookingCountGreaterOperatorType::class, type: 'int')]
36
#[API\Filter(field: 'bookableBookingCount', operator: BookableBookingCountGreaterOrEqualOperatorType::class, type: 'int')]
37
#[API\Filter(field: 'bookableBookingCount', operator: BookableBookingCountLessOperatorType::class, type: 'int')]
38
#[API\Filter(field: 'bookableBookingCount', operator: BookableBookingCountLessOrEqualOperatorType::class, type: 'int')]
39
#[ORM\Entity(BookableRepository::class)]
40
class Bookable extends AbstractModel
41
{
42
    use HasCode;
43
    use HasDescription;
44
    use HasName;
45
    use HasRemarks;
46
47
    #[ORM\Column(type: 'Money', options: ['default' => 0])]
48
    private Money $initialPrice;
49
50
    #[ORM\Column(type: 'Money', options: ['default' => 0])]
51
    private Money $periodicPrice;
52
53
    #[ORM\Column(type: 'Money', nullable: true, options: ['unsigned' => true])]
54
    private ?Money $purchasePrice = null;
55
56
    #[ORM\Column(type: 'smallint', options: ['default' => -1])]
57
    private int $simultaneousBookingMaximum = 1;
58
59
    #[ORM\Column(type: 'smallint', options: ['default' => 0, 'unsigned' => true])]
60
    private int $waitingListLength = 0;
61
62
    #[ORM\Column(type: 'enum', length: 10, options: ['default' => BookingType::AdminApproved])]
63
    private BookingType $bookingType = BookingType::AdminApproved;
64
65
    #[ORM\Column(type: 'enum', options: ['default' => BookableStatus::New])]
66
    private BookableStatus $status = BookableStatus::New;
67
68
    #[ORM\Column(type: 'enum', length: 10, options: ['default' => BookableState::Good])]
69
    private BookableState $state = BookableState::Good;
70
71
    #[ORM\Column(type: 'date', nullable: true)]
72
    private ?ChronosDate $verificationDate = null;
73
74
    /**
75
     * @var Collection<int, BookableTag>
76
     */
77
    #[ORM\ManyToMany(targetEntity: BookableTag::class, mappedBy: 'bookables')]
78
    private Collection $bookableTags;
79
80
    /**
81
     * @var Collection<int, Booking>
82
     */
83
    #[ORM\OneToMany(targetEntity: Booking::class, mappedBy: 'bookable')]
84
    private Collection $bookings;
85
86
    /**
87
     * @var Collection<int, License>
88
     */
89
    #[ORM\ManyToMany(targetEntity: License::class, mappedBy: 'bookables')]
90
    private Collection $licenses;
91
92
    #[ORM\OneToOne(targetEntity: Image::class, orphanRemoval: true)]
93
    #[ORM\JoinColumn(name: 'image_id', referencedColumnName: 'id')]
94
    private ?Image $image = null;
95
96
    #[ORM\JoinColumn(onDelete: 'CASCADE')]
97
    #[ORM\ManyToOne(targetEntity: Account::class)]
98
    private ?Account $creditAccount = null;
99
100 30
    public function __construct()
101
    {
102 30
        $this->initialPrice = Money::CHF(0);
103 30
        $this->periodicPrice = Money::CHF(0);
104
105 30
        $this->bookings = new ArrayCollection();
106 30
        $this->licenses = new ArrayCollection();
107 30
        $this->bookableTags = new ArrayCollection();
108
    }
109
110 17
    public function getBookings(): Collection
111
    {
112 17
        return $this->bookings;
113
    }
114
115
    /**
116
     * Notify the bookable that it has a new booking.
117
     * This should only be called by Booking::addBookable().
118
     */
119 27
    public function bookingAdded(Booking $booking): void
120
    {
121 27
        $this->bookings->add($booking);
122
    }
123
124
    /**
125
     * Notify the bookable that it a booking was removed.
126
     * This should only be called by Booking::removeBookable().
127
     */
128 1
    public function bookingRemoved(Booking $booking): void
129
    {
130 1
        $this->bookings->removeElement($booking);
131
    }
132
133 17
    public function getLicenses(): Collection
134
    {
135 17
        return $this->licenses;
136
    }
137
138
    /**
139
     * Notify the bookable that it has a new license.
140
     * This should only be called by License::addBookable().
141
     */
142 4
    public function licenseAdded(License $license): void
143
    {
144 4
        $this->licenses->add($license);
145
    }
146
147
    /**
148
     * Notify the bookable that it a license was removed.
149
     * This should only be called by License::removeBookable().
150
     */
151 1
    public function licenseRemoved(License $license): void
152
    {
153 1
        $this->licenses->removeElement($license);
154
    }
155
156 6
    public function getInitialPrice(): Money
157
    {
158 6
        return $this->initialPrice;
159
    }
160
161 6
    public function setInitialPrice(Money $initialPrice): void
162
    {
163 6
        $this->initialPrice = $initialPrice;
164
    }
165
166 10
    public function getPeriodicPrice(): Money
167
    {
168 10
        return $this->periodicPrice;
169
    }
170
171 8
    public function setPeriodicPrice(Money $periodicPrice): void
172
    {
173 8
        $this->periodicPrice = $periodicPrice;
174
    }
175
176
    public function getPurchasePrice(): ?Money
177
    {
178
        return $this->purchasePrice;
179
    }
180
181
    public function setPurchasePrice(?Money $purchasePrice): void
182
    {
183
        $this->purchasePrice = $purchasePrice;
184
    }
185
186 14
    public function getSimultaneousBookingMaximum(): int
187
    {
188 14
        return $this->simultaneousBookingMaximum;
189
    }
190
191 14
    public function setSimultaneousBookingMaximum(int $simultaneousBookingMaximum): void
192
    {
193 14
        $this->simultaneousBookingMaximum = $simultaneousBookingMaximum;
194
    }
195
196 28
    public function getBookingType(): BookingType
197
    {
198 28
        return $this->bookingType;
199
    }
200
201
    /**
202
     * Whether this bookable can be bought.
203
     */
204 17
    public function getStatus(): BookableStatus
205
    {
206 17
        return $this->status;
207
    }
208
209
    /**
210
     * Whether this bookable can be bought.
211
     */
212 14
    public function setStatus(BookableStatus $status): void
213
    {
214 14
        $this->status = $status;
215
    }
216
217 2
    public function setBookingType(BookingType $state): void
218
    {
219 2
        $this->bookingType = $state;
220
    }
221
222
    /**
223
     * State of the bookable.
224
     */
225
    public function getState(): BookableState
226
    {
227
        return $this->state;
228
    }
229
230
    /**
231
     * State of the bookable.
232
     */
233
    public function setState(BookableState $state): void
234
    {
235
        $this->state = $state;
236
    }
237
238
    /**
239
     * The date then the bookable was last checked.
240
     */
241
    public function getVerificationDate(): ?ChronosDate
242
    {
243
        return $this->verificationDate;
244
    }
245
246
    /**
247
     * The date then the bookable was last checked.
248
     */
249
    public function setVerificationDate(?ChronosDate $verificationDate): void
250
    {
251
        $this->verificationDate = $verificationDate;
252
    }
253
254 6
    public function getBookableTags(): Collection
255
    {
256 6
        return $this->bookableTags;
257
    }
258
259
    /**
260
     * Notify the user that it has a new bookableTag.
261
     * This should only be called by BookableTag::addUser().
262
     */
263
    public function bookableTagAdded(BookableTag $bookableTag): void
264
    {
265
        $this->bookableTags->add($bookableTag);
266
    }
267
268
    /**
269
     * Notify the user that it a bookableTag was removed.
270
     * This should only be called by BookableTag::removeUser().
271
     */
272
    public function bookableTagRemoved(BookableTag $bookableTag): void
273
    {
274
        $this->bookableTags->removeElement($bookableTag);
275
    }
276
277 1
    public function getImage(): ?Image
278
    {
279 1
        return $this->image;
280
    }
281
282 1
    public function setImage(?Image $image): void
283
    {
284
        // We must trigger lazy loading, otherwise Doctrine will seriously
285
        // mess up lifecycle callbacks and delete unrelated image on disk
286 1
        if ($this->image) {
287 1
            $this->image->getFilename();
288
        }
289
290 1
        $this->image = $image;
291
    }
292
293
    /**
294
     * The account to credit when booking this bookable.
295
     */
296 8
    public function getCreditAccount(): ?Account
297
    {
298 8
        return $this->creditAccount;
299
    }
300
301
    /**
302
     * The account to credit when booking this bookable.
303
     */
304 5
    public function setCreditAccount(?Account $creditAccount): void
305
    {
306 5
        $this->creditAccount = $creditAccount;
307
    }
308
309
    /**
310
     * Returns list of active, non-application, bookings. But only if the bookable has a limit of simultaneous booking. Otherwise, returns empty list.
311
     *
312
     * @return Booking[]
313
     */
314 16
    public function getSimultaneousBookings(): array
315
    {
316
        // Pretend to have no simultaneous bookings to avoid too many SQL queries when we don't really care about it
317 16
        if ($this->avoidTooManySqlQueries()) {
318 3
            return [];
319
        }
320
321
        // Only consider approved and unterminated bookings
322 15
        $bookings = $this->getBookings()->filter(fn (Booking $booking): bool => !$booking->getEndDate() && $booking->getStatus() !== BookingStatus::Application)->toArray();
323
324 15
        return $bookings;
325
    }
326
327
    /**
328
     * Returns list of active, non-application, bookings. But only if the bookable has a limit of simultaneous booking. Otherwise, returns empty list.
329
     *
330
     * @return Booking[]
331
     */
332 16
    public function getSimultaneousApplications(): array
333
    {
334
        // Pretend to have no simultaneous bookings to avoid too many SQL queries when we don't really care about it
335 16
        if ($this->avoidTooManySqlQueries()) {
336 3
            return [];
337
        }
338
339 15
        $bookableType = $this->getBookingType();
340 15
        $bookableTypesAllowed = [BookingType::AdminAssigned, BookingType::Application, BookingType::AdminApproved];
341
342 15
        if (!in_array($bookableType, $bookableTypesAllowed, true)) {
343
            return [];
344
        }
345
346
        // Only consider approved and unterminated bookings
347 15
        $bookings = $this->getBookings()->filter(fn (Booking $booking): bool => !$booking->getEndDate() && $booking->getStatus() === BookingStatus::Application)->toArray();
348
349 15
        return $bookings;
350
    }
351
352 16
    private function avoidTooManySqlQueries(): bool
353
    {
354 16
        $bookableType = $this->getBookingType();
355 16
        $bookableTypesAllowed = [BookingType::AdminAssigned, BookingType::Application, BookingType::AdminApproved];
356
357 16
        return !in_array($bookableType, $bookableTypesAllowed, true);
358
    }
359
360
    /**
361
     * Return a list of effective active bookings including sharing conditions.
362
     *
363
     * Only "admin-assigned" + storage tags are sharable bookables. In this case, a list of bookings is returned.
364
     *
365
     * For other bookable types, returns null
366
     *
367
     * @return Booking[]
368
     */
369 6
    public function getPeriodicPriceDividerBookings(): array
370
    {
371 6
        $isAdminAssigned = $this->getBookingType() === BookingType::AdminAssigned;
372
373 6
        $isTagAllowed = false;
374 6
        $allowedTagIds = [BookableTagRepository::STORAGE_ID, BookableTagRepository::FORMATION_ID, BookableTagRepository::WELCOME_ID];
375 6
        foreach ($this->getBookableTags() as $tag) {
376 2
            if (in_array($tag->getId(), $allowedTagIds, true)) {
377 1
                $isTagAllowed = true;
378
379 1
                break;
380
            }
381
        }
382
383 6
        if (!$isAdminAssigned || !$isTagAllowed) {
384 6
            return [];
385
        }
386
387 1
        $bookings = $this->getBookings()->filter(fn (Booking $booking): bool => !$booking->getEndDate())->toArray();
388
389 1
        return $bookings;
390
    }
391
392
    /**
393
     * If non-zero, it allows creating more application bookings, even if the bookable reached its
394
     * simultaneousBookingMaximum. So in effect, it will be a sort of "waiting list" of people who would like to
395
     * participate in case of someone else canceling their booking.
396
     */
397 12
    public function getWaitingListLength(): int
398
    {
399 12
        return $this->waitingListLength;
400
    }
401
402 14
    public function setWaitingListLength(int $waitingListLength): void
403
    {
404 14
        $this->waitingListLength = $waitingListLength;
405
    }
406
}
407