Completed
Push — master ( a4294a...560d6c )
by Florian
15s queued 12s
created

BillService::getNextCloseOpenTime()   A

Complexity

Conditions 5
Paths 4

Size

Total Lines 21
Code Lines 12

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 12
c 0
b 0
f 0
dl 0
loc 21
rs 9.5555
cc 5
nc 4
nop 1
1
<?php
2
3
/*
4
 * This file is part of the vseth-musikzimmer-pay 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\User;
15
use App\Enum\RoomType;
16
use App\Enum\UserCategoryType;
17
use App\Helper\DateTimeHelper;
18
use App\Model\Bill;
19
use App\Model\Bill\Reservation;
20
use App\Model\Bill\Subscription;
21
use App\Service\Interfaces\BillServiceInterface;
22
use App\Service\Interfaces\SettingsServiceInterface;
23
use Symfony\Contracts\Translation\TranslatorInterface;
24
25
class BillService implements BillServiceInterface
26
{
27
    /**
28
     * @var SettingsServiceInterface
29
     */
30
    private $settingService;
31
32
    /**
33
     * @var TranslatorInterface
34
     */
35
    private $translatorService;
36
37
    /**
38
     * BillService constructor.
39
     */
40
    public function __construct(SettingsServiceInterface $settingService, TranslatorInterface $translatorService)
41
    {
42
        $this->settingService = $settingService;
43
        $this->translatorService = $translatorService;
44
    }
45
46
    /**
47
     * @throws \Exception
48
     *
49
     * @return Bill
50
     */
51
    public function createBill(User $user)
52
    {
53
        $bill = new Bill();
54
55
        $bill->setRecipient($user->createRecipient());
56
57
        $setting = $this->settingService->get();
58
        $bill->setId($setting->getPaymentPrefix() . '-' . $user->getId() . '-' . $user->getPaymentRemainder()->getId());
59
        $bill->setPeriodStart($setting->getPeriodStart());
60
        $bill->setPeriodEnd($setting->getPeriodEnd());
61
        $bill->setCategory($user->getCategory());
62
63
        $reservations = $this->getReservations($user->getReservations()->toArray(), $user->getCategory(), $reservationsSubtotal);
64
        $bill->setReservations($reservations);
65
        $bill->setReservationsSubtotal($reservationsSubtotal);
66
        $bill->setTotal($bill->getTotal() + $reservationsSubtotal);
67
68
        $bill->setLastPayedSubscriptionEnd($user->getLastPayedPeriodicFeeEnd());
69
        $subscriptions = $this->getSubscriptions($reservations, $user->getLastPayedPeriodicFeeEnd(), $user->getCategory(), $subscriptionsSubtotal);
70
        $bill->setSubscriptions($subscriptions);
71
        $bill->setSubscriptionsSubtotal($subscriptionsSubtotal);
72
        $bill->setTotal($bill->getTotal() + $subscriptionsSubtotal);
73
74
        $remainderFee = $user->getPaymentRemainder()->getFee();
75
        $bill->setBillFee($remainderFee);
76
        $bill->setTotal($bill->getTotal() + $remainderFee);
77
78
        $bill->setDiscount($user->getDiscount());
79
        $bill->setDiscountDescription($user->getDiscountDescription());
80
        $bill->setTotal($bill->getTotal() - $user->getDiscount());
81
82
        return $bill;
83
    }
84
85
    /**
86
     * @throws \Exception
87
     *
88
     * @return int
89
     */
90
    public function getAmountOwed(User $user)
91
    {
92
        $reservations = $this->getReservations($user->getReservations()->toArray(), $user->getCategory(), $reservationsSubtotal);
93
94
        $this->getSubscriptions($reservations, $user->getLastPayedPeriodicFeeEnd(), $user->getCategory(), $subscriptionsSubtotal);
95
96
        return $reservationsSubtotal + $subscriptionsSubtotal;
97
    }
98
99
    /**
100
     * @param \App\Entity\Reservation[] $splittedReservations
101
     * @param int $reservationTotal
102
     *
103
     * @return Reservation[]
104
     */
105
    public function getReservations(array $reservations, int $userCategory, &$reservationTotal)
106
    {
107
        $reservationTotal = 0;
108
        $models = [];
109
110
        /** @var \App\Entity\Reservation[] $splittedReservations */
111
        $splittedReservations = [];
112
        foreach ($reservations as $reservation) {
113
            /** @var \App\Entity\Reservation $reservation */
114
            if (!$this->isRoomMoreExpensiveDuringOpeningTime($reservation->getRoom())) {
115
                $splittedReservations[] = $reservation;
116
                continue;
117
            }
118
119
            $remainingReservation = $reservation;
120
            if ($reservation->getStart() > $reservation->getEnd()) {
121
                throw new \Exception('start time must be before end time');
122
            }
123
124
            while (true) {
125
                [$nextCloseOpenTime] = $this->getNextCloseOpenTime($remainingReservation->getStart());
126
127
                // no more changes of reservation fee; hence break
128
                if ($nextCloseOpenTime > $remainingReservation->getEnd()) {
129
                    $splittedReservations[] = $remainingReservation;
130
                    break;
131
                }
132
133
                $currentReservation = clone $remainingReservation;
134
                $currentReservation->setEnd($nextCloseOpenTime);
0 ignored issues
show
Bug introduced by
It seems like $nextCloseOpenTime can also be of type false; however, parameter $end of App\Entity\Reservation::setEnd() does only seem to accept DateTime, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

134
                $currentReservation->setEnd(/** @scrutinizer ignore-type */ $nextCloseOpenTime);
Loading history...
135
                $splittedReservations[] = $currentReservation;
136
137
                $remainingReservation->setStart($currentReservation->getEnd());
138
            }
139
        }
140
141
        foreach ($splittedReservations as $reservation) {
142
            $model = new Reservation();
143
144
            $model->setStartAt($reservation->getStart());
145
            $model->setEndAt($reservation->getEnd());
146
147
            $room = RoomType::getTranslation($reservation->getRoom(), $this->translatorService);
148
            $model->setRoom($room);
149
150
            [, $isCurrentlyWithinOpeningTimes] = $this->getNextCloseOpenTime($reservation->getStart());
151
            $model->setPricePerHour($this->getPricePerHour($reservation->getRoom(), $userCategory, $isCurrentlyWithinOpeningTimes));
0 ignored issues
show
Bug introduced by
$isCurrentlyWithinOpeningTimes of type integer is incompatible with the type boolean expected by parameter $isWithinOpeningTimes of App\Service\BillService::getPricePerHour(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

151
            $model->setPricePerHour($this->getPricePerHour($reservation->getRoom(), $userCategory, /** @scrutinizer ignore-type */ $isCurrentlyWithinOpeningTimes));
Loading history...
152
153
            $duration = $model->getStartAt()->diff($model->getEndAt());
154
            $hours = $this->getTotalHours($duration);
0 ignored issues
show
Bug introduced by
It seems like $duration can also be of type false; however, parameter $int of App\Service\BillService::getTotalHours() does only seem to accept DateInterval, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

154
            $hours = $this->getTotalHours(/** @scrutinizer ignore-type */ $duration);
Loading history...
155
            $model->setTotal($hours * $model->getPricePerHour());
156
157
            $reservationTotal += $model->getTotal();
158
            $models[] = $model;
159
        }
160
161
        return $models;
162
    }
163
164
    /**
165
     * @param Reservation[] $reservations
166
     * @param \DateTime $lastPayedPeriodicFeeEnd
167
     * @param int $subscriptionTotal
168
     *
169
     * @throws \Exception
170
     *
171
     * @return Subscription[]
172
     */
173
    private function getSubscriptions(array $reservations, ?\DateTime $lastPayedPeriodicFeeEnd, int $userCategory, &$subscriptionTotal)
174
    {
175
        $subscriptionTotal = 0;
176
177
        $dateFormat = 'Y-m-d';
178
        $currentLastValidDate = $lastPayedPeriodicFeeEnd !== null ? $lastPayedPeriodicFeeEnd->format($dateFormat) : '';
179
        /** @var Subscription[] $subscriptions */
180
        $subscriptions = [];
181
        foreach ($reservations as $reservation) {
182
            $currentDate = $reservation->getStartAt()->format($dateFormat);
183
            if ($currentDate <= $currentLastValidDate) {
184
                continue;
185
            }
186
187
            $subscription = new Subscription();
188
            $subscription->setPrice($this->getSubscriptionPrice($userCategory));
189
190
            $subscription->setStartAt(new \DateTime($currentDate));
191
            $subscription->setEndAt(DateTimeHelper::getSubscriptionEnd(clone $subscription->getStartAt()));
192
193
            $subscriptionTotal += $subscription->getPrice();
194
            $subscriptions[] = $subscription;
195
196
            $currentLastValidDate = $subscription->getEndAt()->format($dateFormat);
197
        }
198
199
        return $subscriptions;
200
    }
201
202
    /**
203
     * @return float|int
204
     */
205
    private function getTotalHours(\DateInterval $int)
206
    {
207
        return ($int->days * 24) + $int->h;
208
    }
209
210
    /**
211
     * @return int
212
     */
213
    private function getPricePerHour(int $room, int $userCategory, bool $isWithinOpeningTimes)
214
    {
215
        // bandraum
216
        if ($this->isRoomMoreExpensiveDuringOpeningTime($room) && $isWithinOpeningTimes) {
217
            switch ($userCategory) {
218
                case UserCategoryType::STUDENT:
219
                    return 5;
220
                case UserCategoryType::PHD:
221
                    return 10;
222
                case UserCategoryType::ETH_UNIVERSITY_STAFF:
223
                    return 20;
224
                default:
225
                    return 30;
226
            }
227
        }
228
229
        switch ($userCategory) {
230
            case UserCategoryType::STUDENT:
231
                return 1;
232
            case UserCategoryType::PHD:
233
                return 3;
234
            case UserCategoryType::ETH_UNIVERSITY_STAFF:
235
                return 6;
236
            default:
237
                return 8;
238
        }
239
    }
240
241
    private function isRoomMoreExpensiveDuringOpeningTime(int $room)
242
    {
243
        return $room === RoomType::HPI_D_5_2;
244
    }
245
246
    /**
247
     * @param int $room
248
     *
249
     * @return int
250
     */
251
    private function getSubscriptionPrice(int $userCategory)
252
    {
253
        switch ($userCategory) {
254
            case UserCategoryType::STUDENT:
255
                return 25;
256
            case UserCategoryType::PHD:
257
                return 40;
258
            case UserCategoryType::ETH_UNIVERSITY_STAFF:
259
                return 50;
260
            default:
261
                return 60;
262
        }
263
    }
264
265
    private function getNextCloseOpenTime(\DateTime $dateTime)
266
    {
267
        $weekday = $dateTime->format('N');
268
269
        if ($weekday >= 1 && $weekday <= 5) {
270
            //is monday - friday
271
            $openTime = (clone $dateTime)->setTime(07, 00);
272
            $closeTime = (clone $dateTime)->setTime(18, 00);
273
274
            if ($dateTime < $openTime) {
275
                return [$openTime, 0];
276
            } elseif ($dateTime < $closeTime) {
277
                return [$closeTime, 1];
278
            }
279
        }
280
281
        $dateTime = clone $dateTime;
282
        $dateTime->add(new \DateInterval('P1D'));
283
        $dateTime->setTime(0, 0);
284
285
        return $this->getNextCloseOpenTime($dateTime);
286
    }
287
}
288