Passed
Push — master ( f3b4d6...85dd39 )
by Sam
11:35
created

Invoicer::updateOrderLine()   A

Complexity

Conditions 5
Paths 8

Size

Total Lines 26
Code Lines 20

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 18
CRAP Score 5.0035

Importance

Changes 0
Metric Value
eloc 20
dl 0
loc 26
ccs 18
cts 19
cp 0.9474
rs 9.2888
c 0
b 0
f 0
cc 5
nc 8
nop 7
crap 5.0035
1
<?php
2
3
declare(strict_types=1);
4
5
namespace Application\Service;
6
7
use Application\Model\AbstractProduct;
8
use Application\Model\Order;
9
use Application\Model\OrderLine;
10
use Application\Model\Product;
11
use Application\Model\Subscription;
12
use Application\Model\User;
13
use Application\Repository\UserRepository;
14
use Doctrine\ORM\EntityManager;
15
use Ecodev\Felix\Api\Exception;
16
use Money\Money;
17
18
/**
19
 * Service to create order and transactions for products and their quantity.
20
 */
21
class Invoicer
22
{
23
    /**
24
     * @var EntityManager
25
     */
26
    private $entityManager;
27
28
    /**
29
     * @var UserRepository
30
     */
31
    private $userRepository;
32
33 1
    public function __construct(EntityManager $entityManager)
34
    {
35 1
        $this->entityManager = $entityManager;
36 1
        $this->userRepository = $this->entityManager->getRepository(User::class);
37 1
    }
38
39 14
    public function createOrder(array $orderInput): ?Order
40
    {
41 14
        $lines = $orderInput['orderLines'];
42 14
        if (!$lines) {
43
            return null;
44
        }
45
46 14
        $order = new Order();
47 14
        $order->setPaymentMethod($orderInput['paymentMethod']);
48 14
        $order->setFirstName($orderInput['firstName'] ?? '');
49 14
        $order->setLastName($orderInput['lastName'] ?? '');
50 14
        $order->setStreet($orderInput['street'] ?? '');
51 14
        $order->setLocality($orderInput['locality'] ?? '');
52 14
        $order->setPostcode($orderInput['postcode'] ?? '');
53 14
        $order->setCountry($orderInput['country'] ?? null);
54
55 14
        $this->userRepository->getAclFilter()->runWithoutAcl(function () use ($lines, $order): void {
56 14
            $this->entityManager->persist($order);
57
58 14
            $total = Money::CHF(0);
59 14
            foreach ($lines as $line) {
60 14
                $args = $this->extractArgsFromLine($line);
61
62 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

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

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