Issues (65)

server/Application/Service/Invoicer.php (2 issues)

1
<?php
2
3
declare(strict_types=1);
4
5
namespace Application\Service;
6
7
use Application\Enum\BookingStatus;
8
use Application\Enum\BookingType;
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
    private int $count = 0;
27
28
    private readonly BookingRepository $bookingRepository;
29
30 1
    public function __construct(
31
        private readonly EntityManager $entityManager,
32
    ) {
33 1
        $this->bookingRepository = $this->entityManager->getRepository(Booking::class);
0 ignored issues
show
The property bookingRepository is declared read-only in Application\Service\Invoicer.
Loading history...
34
    }
35
36 1
    public function invoicePeriodic(?User $onlyUser = null): int
37
    {
38 1
        $this->count = 0;
39
40 1
        $this->bookingRepository->getAclFilter()->runWithoutAcl(function () use ($onlyUser): void {
41 1
            $bookings = $this->bookingRepository->getAllToInvoice($onlyUser);
42
43 1
            $user = null;
44 1
            $bookingPerUser = [];
45
46
            /** @var Booking $booking */
47 1
            foreach ($bookings as $booking) {
48 1
                $nextUser = $booking->getOwner();
49 1
                if ($user !== $nextUser) {
50 1
                    $this->createTransaction($user, $bookingPerUser, false);
51
52 1
                    $user = $nextUser;
53 1
                    $bookingPerUser = [];
54
                }
55
56 1
                $bookingPerUser[] = $booking;
57
            }
58 1
            $this->createTransaction($user, $bookingPerUser, false);
59 1
        });
60
61 1
        return $this->count;
62
    }
63
64 23
    public function invoiceInitial(User $user, Booking $booking, ?BookingStatus $previousStatus): int
65
    {
66 23
        $this->count = 0;
67 23
        $this->bookingRepository->getAclFilter()->runWithoutAcl(function () use ($user, $booking, $previousStatus): void {
68 23
            if ($this->shouldInvoiceInitial($booking, $previousStatus)) {
69 11
                $this->createTransaction($user, [$booking], true);
70
            }
71 23
        });
72
73 23
        return $this->count;
74
    }
75
76 23
    private function shouldInvoiceInitial(Booking $booking, ?BookingStatus $previousStatus): bool
77
    {
78 23
        $bookable = $booking->getBookable();
79
80
        // Only invoice a booking that is really booked or processed (and not an application)
81 23
        if (!in_array($booking->getStatus(), [BookingStatus::Booked, BookingStatus::Processed], true)) {
82 8
            return false;
83
        }
84
85
        // Cannot invoice if we don't know where the money goes
86 20
        if (!$bookable->getCreditAccount()) {
87 2
            return false;
88
        }
89
90
        // Nothing to invoice if the bookable is free
91 18
        if ($bookable->getInitialPrice()->isZero() && $bookable->getPeriodicPrice()->isZero()) {
92 2
            return false;
93
        }
94
95
        // Never invoice bookings of application type bookable, because they are only used to request the admin to create the actual booking
96 16
        if ($bookable->getBookingType() === BookingType::Application) {
97 1
            return false;
98
        }
99
100
        // If a booking status has been changed from `APPLICATION to `BOOKED` or `PROCESSED`, then it is OK to invoice
101 15
        if ($previousStatus === BookingStatus::Application) {
102 6
            return true;
103
        }
104
105
        // Otherwise, never invoice a booking that is updated, and only invoice a booking that is created right now
106 9
        if ($booking->getId()) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $booking->getId() of type integer|null is loosely compared to true; this is ambiguous if the integer can be 0. You might want to explicitly use !== null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For integer values, zero is a special case, in particular the following results might be unexpected:

0   == false // true
0   == null  // true
123 == false // false
123 == null  // false

// It is often better to use strict comparison
0 === false // false
0 === null  // false
Loading history...
107 4
            return false;
108
        }
109
110 5
        return true;
111
    }
112
113 12
    private function createTransaction(?User $user, array $bookings, bool $isInitial): void
114
    {
115 12
        if (!$user || !$bookings) {
116 1
            return;
117
        }
118
119
        /** @var AccountRepository $accountRepository */
120 12
        $accountRepository = $this->entityManager->getRepository(Account::class);
121 12
        $account = $accountRepository->getOrCreate($user);
122 12
        $transaction = new Transaction();
123 12
        $transaction->setTransactionDate(Chronos::now());
124 12
        $transaction->setName('Cotisation et services ' . Chronos::today()->format('Y'));
125 12
        $this->entityManager->persist($transaction);
126
127 12
        foreach ($bookings as $booking) {
128 12
            $bookable = $booking->getBookable();
129 12
            if ($isInitial) {
130 11
                $balance = $this->calculateInitialBalance($booking);
131 11
                $this->createTransactionLine($transaction, $bookable, $account, $balance, 'Prestation ponctuelle', $user->getName());
132
            }
133
134 12
            $balance = $this->calculatePeriodicBalance($booking);
135 12
            $this->createTransactionLine($transaction, $bookable, $account, $balance, 'Prestation annuelle', $user->getName());
136
        }
137
138 12
        ++$this->count;
139
    }
140
141 11
    private function calculateInitialBalance(Booking $booking): Money
142
    {
143 11
        $bookable = $booking->getBookable();
144
145
        // TODO: https://support.ecodev.ch/issues/6227
146
147 11
        return $bookable->getInitialPrice();
148
    }
149
150 12
    private function calculatePeriodicBalance(Booking $booking): Money
151
    {
152 12
        return $booking->getPeriodicPrice();
153
    }
154
155 12
    private function createTransactionLine(Transaction $transaction, Bookable $bookable, Account $account, Money $balance, string $name, string $remarks = ''): void
156
    {
157 12
        if ($balance->isPositive()) {
158 11
            $debit = $account;
159 11
            $credit = $bookable->getCreditAccount();
160 5
        } elseif ($balance->isNegative()) {
161 1
            $debit = $bookable->getCreditAccount();
162 1
            $credit = $account;
163 1
            $balance = $balance->absolute();
164
        } else {
165
            // Never create a line with 0 balance
166 4
            return;
167
        }
168
169 12
        $transactionLine = new TransactionLine();
170 12
        $this->entityManager->persist($transactionLine);
171
172 12
        $transactionLine->setName($name);
173 12
        $transactionLine->setBookable($bookable);
174 12
        $transactionLine->setDebit($debit);
175 12
        $transactionLine->setCredit($credit);
176 12
        $transactionLine->setBalance($balance);
177 12
        $transactionLine->setTransaction($transaction);
178 12
        $transactionLine->setTransactionDate(Chronos::now());
179 12
        $transactionLine->setRemarks($remarks);
180
    }
181
}
182