Failed Conditions
Push — master ( 4369d4...a5c076 )
by Adrien
15:03
created

Bookable::getCreditAccount()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 2

Importance

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