Passed
Push — master ( 38d154...429a57 )
by Adrien
07:44
created

Invoicer::shouldInvoiceInitial()   B

Complexity

Conditions 8
Paths 7

Size

Total Lines 35
Code Lines 14

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 15
CRAP Score 8

Importance

Changes 0
Metric Value
eloc 14
c 0
b 0
f 0
dl 0
loc 35
rs 8.4444
ccs 15
cts 15
cp 1
cc 8
nc 7
nop 2
crap 8
1
<?php
2
3
declare(strict_types=1);
4
5
namespace Application\Service;
6
7
use Application\DBAL\Types\BookingStatusType;
8
use Application\DBAL\Types\BookingTypeType;
9
use Application\Model\Account;
10
use Application\Model\Bookable;
11
use Application\Model\Booking;
12
use Application\Model\Transaction;
13
use Application\Model\TransactionLine;
14
use Application\Model\User;
15
use Application\Repository\AccountRepository;
16
use Application\Repository\BookingRepository;
17
use Cake\Chronos\Chronos;
18
use Doctrine\ORM\EntityManager;
19
use Money\Money;
20
21
/**
22
 * Service to create transactions for non-free booking, if needed, for all users or one user
23
 */
24
class Invoicer
25
{
26
    /**
27
     * @var EntityManager
28
     */
29
    private $entityManager;
30
31
    /**
32
     * @var int
33
     */
34
    private $count = 0;
35
36
    /**
37
     * @var BookingRepository
38
     */
39
    private $bookingRepository;
40
41 1
    public function __construct(EntityManager $entityManager)
42
    {
43 1
        $this->entityManager = $entityManager;
44 1
        $this->bookingRepository = $this->entityManager->getRepository(Booking::class);
45 1
    }
46
47 1
    public function invoicePeriodic(?User $onlyUser = null): int
48
    {
49 1
        $this->count = 0;
50
51 1
        $this->bookingRepository->getAclFilter()->runWithoutAcl(function () use ($onlyUser): void {
52 1
            $bookings = $this->bookingRepository->getAllToInvoice($onlyUser);
53
54 1
            $user = null;
55 1
            $bookingPerUser = [];
56
57
            /** @var Booking $booking */
58 1
            foreach ($bookings as $booking) {
59 1
                $nextUser = $booking->getOwner();
60 1
                if ($user !== $nextUser) {
61 1
                    $this->createTransaction($user, $bookingPerUser, false);
62
63 1
                    $user = $nextUser;
64 1
                    $bookingPerUser = [];
65
                }
66
67 1
                $bookingPerUser[] = $booking;
68
            }
69 1
            $this->createTransaction($user, $bookingPerUser, false);
70 1
        });
71
72 1
        return $this->count;
73
    }
74
75 22
    public function invoiceInitial(User $user, Booking $booking, ?string $previousStatus): int
76
    {
77 22
        $this->count = 0;
78 22
        $this->bookingRepository->getAclFilter()->runWithoutAcl(function () use ($user, $booking, $previousStatus): void {
79 22
            if ($this->shouldInvoiceInitial($booking, $previousStatus)) {
80 10
                $this->createTransaction($user, [$booking], true);
81
            }
82 22
        });
83
84 22
        return $this->count;
85
    }
86
87 22
    private function shouldInvoiceInitial(Booking $booking, ?string $previousStatus): bool
88
    {
89 22
        $bookable = $booking->getBookable();
90
91
        // Only invoice a booking that is really booked (and not an application to be booked)
92 22
        if ($booking->getStatus() !== BookingStatusType::BOOKED) {
93 8
            return false;
94
        }
95
96
        // Cannot invoice if we don't know where the money goes
97 19
        if (!$bookable->getCreditAccount()) {
98 2
            return false;
99
        }
100
101
        // Nothing to invoice if the bookable is free
102 17
        if ($bookable->getInitialPrice()->isZero() && $bookable->getPeriodicPrice()->isZero()) {
103 2
            return false;
104
        }
105
106
        // Never invoice bookings of application type bookable, because they are only used to request the admin to create the actual booking
107 15
        if ($bookable->getBookingType() === BookingTypeType::APPLICATION) {
108 1
            return false;
109
        }
110
111
        // If a booking status has been changed from `APPLICATION to `BOOKED`, then it is OK to invoice
112 14
        if ($previousStatus === BookingStatusType::APPLICATION) {
113 6
            return true;
114
        }
115
116
        // Otherwise, never invoice a booking that is updated, and only invoice a booking that is created right now
117 8
        if ($booking->getId()) {
118 4
            return false;
119
        }
120
121 4
        return true;
122
    }
123
124 11
    private function createTransaction(?User $user, array $bookings, bool $isInitial): void
125
    {
126 11
        if (!$user || !$bookings) {
127 1
            return;
128
        }
129
130
        /** @var AccountRepository $accountRepository */
131 11
        $accountRepository = $this->entityManager->getRepository(Account::class);
132 11
        $account = $accountRepository->getOrCreate($user);
133 11
        $transaction = new Transaction();
134 11
        $transaction->setTransactionDate(Chronos::now());
135 11
        $transaction->setName('Cotisation et services ' . Chronos::today()->format('Y'));
136 11
        $this->entityManager->persist($transaction);
137
138 11
        foreach ($bookings as $booking) {
139 11
            $bookable = $booking->getBookable();
140 11
            if ($isInitial) {
141 10
                $balance = $this->calculateInitialBalance($booking);
142 10
                $this->createTransactionLine($transaction, $bookable, $account, $balance, 'Prestation ponctuelle', $user->getName());
143
            }
144
145 11
            $balance = $this->calculatePeriodicBalance($booking);
146 11
            $this->createTransactionLine($transaction, $bookable, $account, $balance, 'Prestation annuelle', $user->getName());
147
        }
148
149 11
        ++$this->count;
150 11
    }
151
152 10
    private function calculateInitialBalance(Booking $booking): Money
153
    {
154 10
        $bookable = $booking->getBookable();
155
156
        // TODO: https://support.ecodev.ch/issues/6227
157
158 10
        return $bookable->getInitialPrice();
159
    }
160
161 11
    private function calculatePeriodicBalance(Booking $booking): Money
162
    {
163 11
        return $booking->getPeriodicPrice();
164
    }
165
166 11
    private function createTransactionLine(Transaction $transaction, Bookable $bookable, Account $account, Money $balance, string $name, string $remarks = ''): void
167
    {
168 11
        if ($balance->isPositive()) {
169 10
            $debit = $account;
170 10
            $credit = $bookable->getCreditAccount();
171 5
        } elseif ($balance->isNegative()) {
172 1
            $debit = $bookable->getCreditAccount();
173 1
            $credit = $account;
174 1
            $balance = $balance->absolute();
175
        } else {
176
            // Never create a line with 0 balance
177 4
            return;
178
        }
179
180 11
        $transactionLine = new TransactionLine();
181 11
        $this->entityManager->persist($transactionLine);
182
183 11
        $transactionLine->setName($name);
184 11
        $transactionLine->setBookable($bookable);
185 11
        $transactionLine->setDebit($debit);
186 11
        $transactionLine->setCredit($credit);
187 11
        $transactionLine->setBalance($balance);
188 11
        $transactionLine->setTransaction($transaction);
189 11
        $transactionLine->setTransactionDate(Chronos::now());
190 11
        $transactionLine->setRemarks($remarks);
191 11
    }
192
}
193