Invoicer::__construct()   A
last analyzed

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 2
CRAP Score 1

Importance

Changes 0
Metric Value
eloc 1
dl 0
loc 3
ccs 2
cts 2
cp 1
rs 10
c 0
b 0
f 0
cc 1
nc 1
nop 1
crap 1
1
<?php
2
3
declare(strict_types=1);
4
5
namespace Application\Service;
6
7
use Application\Enum\ProductType;
8
use Application\Model\AbstractProduct;
9
use Application\Model\Order;
10
use Application\Model\OrderLine;
11
use Application\Model\Product;
12
use Application\Model\Subscription;
13
use Application\Model\User;
14
use Application\Repository\UserRepository;
15
use Doctrine\ORM\EntityManager;
16
use Ecodev\Felix\Api\Exception;
17
use Money\Money;
18
19
/**
20
 * Service to create order and transactions for products and their quantity.
21
 */
22
class Invoicer
23
{
24
    private readonly UserRepository $userRepository;
25
26 1
    public function __construct(private readonly EntityManager $entityManager)
27
    {
28 1
        $this->userRepository = $this->entityManager->getRepository(User::class);
0 ignored issues
show
Bug introduced by
The property userRepository is declared read-only in Application\Service\Invoicer.
Loading history...
29
    }
30
31 14
    public function createOrder(array $orderInput): ?Order
32
    {
33 14
        $lines = $orderInput['orderLines'];
34 14
        if (!$lines) {
35
            return null;
36
        }
37
38 14
        $order = new Order();
39 14
        $order->setPaymentMethod($orderInput['paymentMethod']);
40 14
        $order->setFirstName($orderInput['firstName'] ?? '');
41 14
        $order->setLastName($orderInput['lastName'] ?? '');
42 14
        $order->setStreet($orderInput['street'] ?? '');
43 14
        $order->setLocality($orderInput['locality'] ?? '');
44 14
        $order->setPostcode($orderInput['postcode'] ?? '');
45 14
        $order->setCountry($orderInput['country'] ?? null);
46
47 14
        $this->userRepository->getAclFilter()->runWithoutAcl(function () use ($lines, $order): void {
48 14
            $this->entityManager->persist($order);
49
50 14
            $total = Money::CHF(0);
51 14
            foreach ($lines as $line) {
52 14
                $args = $this->extractArgsFromLine($line);
53
54 13
                $orderLine = $this->createOrderLine($order, ...$args);
0 ignored issues
show
Bug introduced by
It seems like $args can also be of type Money\Money; however, parameter $product of Application\Service\Invoicer::createOrderLine() does only seem to accept Application\Model\AbstractProduct|null, 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

54
                $orderLine = $this->createOrderLine($order, /** @scrutinizer ignore-type */ ...$args);
Loading history...
55 13
                $total = $total->add($orderLine->getBalanceCHF());
56
            }
57 14
        });
58
59 13
        return $order;
60
    }
61
62 13
    private function createOrderLine(Order $order, ?AbstractProduct $product, Money $pricePerUnit, int $quantity, bool $isCHF, ProductType $type, array $additionalEmails): OrderLine
63
    {
64 13
        $orderLine = new OrderLine();
65 13
        $this->entityManager->persist($orderLine);
66 13
        $orderLine->setOrder($order);
67
68 13
        $this->updateOrderLine($orderLine, $product, $pricePerUnit, $quantity, $isCHF, $type, $additionalEmails);
69
70 13
        return $orderLine;
71
    }
72
73 15
    private function extractArgsFromLine($input): array
74
    {
75 15
        $product = $input['product'] ?? null;
76 15
        $subscription = $input['subscription'] ?? null;
77 15
        $quantity = $input['quantity'];
78 15
        $isCHF = $input['isCHF'];
79 15
        $type = $input['type'];
80 15
        $additionalEmails = $input['additionalEmails'];
81 15
        $pricePerUnit = $input['pricePerUnit'] ?? null;
82 15
        $this->assertExactlyOneNotNull($product, $subscription, $pricePerUnit);
83
84 15
        $abstractProduct = $product ?? $subscription;
85 15
        $pricePerUnit = $this->getPricePerUnit($abstractProduct, $pricePerUnit, $isCHF);
86
87 14
        if ($additionalEmails && !$subscription) {
88
            throw new Exception('Cannot submit additionalEmails without a subscription');
89
        }
90
91 14
        if ($additionalEmails && !$subscription->isPro()) {
92
            throw new Exception('Cannot submit additionalEmails with a subscription that is not pro');
93
        }
94
95
        // User cannot choose type of a subscription
96 14
        if ($subscription) {
97 2
            $type = $subscription->getType();
98
        }
99
100 14
        return [
101 14
            $abstractProduct,
102 14
            $pricePerUnit,
103 14
            $quantity,
104 14
            $isCHF,
105 14
            $type,
106 14
            $additionalEmails,
107 14
        ];
108
    }
109
110 15
    private function assertExactlyOneNotNull(...$args): void
111
    {
112 15
        $onlyNotNull = array_filter($args, fn ($val) => $val !== null);
113
114 15
        if (count($onlyNotNull) !== 1) {
115
            throw new Exception('Must have a product, or a subscription, or a pricePerUnit. And not a mixed of those.');
116
        }
117
    }
118
119 4
    public function updateOrderLineAndTransactionLine(OrderLine $orderLine, array $line): void
120
    {
121 4
        $this->userRepository->getAclFilter()->runWithoutAcl(function () use ($orderLine, $line): void {
122 4
            $args = $this->extractArgsFromLine($line);
123 4
            $this->updateOrderLine($orderLine, ...$args);
0 ignored issues
show
Bug introduced by
It seems like $args can also be of type Money\Money; however, parameter $product of Application\Service\Invoicer::updateOrderLine() does only seem to accept Application\Model\AbstractProduct|null, 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

123
            $this->updateOrderLine($orderLine, /** @scrutinizer ignore-type */ ...$args);
Loading history...
124 4
        });
125
    }
126
127 14
    private function updateOrderLine(OrderLine $orderLine, ?AbstractProduct $product, Money $pricePerUnit, int $quantity, bool $isCHF, ProductType $type, array $additionalEmails): void
128
    {
129 14
        if ($isCHF) {
130 13
            $balanceCHF = $pricePerUnit->multiply($quantity);
131 13
            $balanceEUR = Money::EUR(0);
132
        } else {
133 2
            $balanceCHF = Money::CHF(0);
134 2
            $balanceEUR = $pricePerUnit->multiply($quantity);
135
        }
136
137 14
        if (!$product) {
138 3
            $orderLine->setDonation();
139 11
        } elseif ($product instanceof Product) {
140 9
            $orderLine->setProduct($product);
141 2
        } elseif ($product instanceof Subscription) {
142 2
            $orderLine->setSubscription($product);
143
        } else {
144
            throw new Exception('Unsupported subclass of product');
145
        }
146
147 14
        $orderLine->setIsCHF($isCHF);
148 14
        $orderLine->setType($type);
149 14
        $orderLine->setQuantity($quantity);
150 14
        $orderLine->setBalanceCHF($balanceCHF);
151 14
        $orderLine->setBalanceEUR($balanceEUR);
152 14
        $orderLine->setAdditionalEmails($additionalEmails);
153
    }
154
155 15
    private function getPricePerUnit(?AbstractProduct $product, ?Money $pricePerUnit, bool $isCHF): Money
156
    {
157 15
        if ($product && $isCHF) {
158 11
            return $product->getPricePerUnitCHF();
159
        }
160
161 5
        if ($product) {
162 1
            return $product->getPricePerUnitEUR();
163
        }
164
165 4
        if ($pricePerUnit === null || !$pricePerUnit->isPositive()) {
166 1
            throw new Exception('A donation must have strictly positive price');
167
        }
168
169
        // The API always assume CHF, but if the client specifically say it is EUR, we need to convert
170 3
        if (!$isCHF) {
171 1
            return Money::EUR($pricePerUnit->getAmount());
172
        }
173
174 2
        return $pricePerUnit;
175
    }
176
}
177