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