Passed
Push — master ( f91cc7...aafc79 )
by Adrien
10:59
created

Invoicer::createOrder()   A

Complexity

Conditions 3
Paths 2

Size

Total Lines 29
Code Lines 19

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 20
CRAP Score 3.0009

Importance

Changes 0
Metric Value
cc 3
eloc 19
nc 2
nop 1
dl 0
loc 29
ccs 20
cts 21
cp 0.9524
crap 3.0009
rs 9.6333
c 0
b 0
f 0
1
<?php
2
3
declare(strict_types=1);
4
5
namespace Application\Service;
6
7
use Application\DBAL\Types\ProductTypeType;
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
    /**
25
     * @var EntityManager
26
     */
27
    private $entityManager;
28
29
    /**
30
     * @var UserRepository
31
     */
32
    private $userRepository;
33
34 1
    public function __construct(EntityManager $entityManager)
35
    {
36 1
        $this->entityManager = $entityManager;
37 1
        $this->userRepository = $this->entityManager->getRepository(User::class);
38 1
    }
39
40 11
    public function createOrder(array $orderInput): ?Order
41
    {
42 11
        $lines = $orderInput['orderLines'];
43 11
        if (!$lines) {
44
            return null;
45
        }
46
47 11
        $order = new Order();
48 11
        $order->setPaymentMethod($orderInput['paymentMethod']);
49 11
        $order->setFirstName($orderInput['firstName'] ?? '');
50 11
        $order->setLastName($orderInput['lastName'] ?? '');
51 11
        $order->setStreet($orderInput['street'] ?? '');
52 11
        $order->setLocality($orderInput['locality'] ?? '');
53 11
        $order->setPostcode($orderInput['postcode'] ?? '');
54 11
        $order->setCountry($orderInput['country'] ?? null);
55
56 11
        $this->userRepository->getAclFilter()->runWithoutAcl(function () use ($lines, $order): void {
57 11
            $this->entityManager->persist($order);
58
59 11
            $total = Money::CHF(0);
60 11
            foreach ($lines as $line) {
61 11
                $args = $this->extractArgsFromLine($line);
62
63 11
                $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

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

134
            $this->updateOrderLine($orderLine, /** @scrutinizer ignore-type */ ...$args);
Loading history...
135 4
        });
136 4
    }
137
138 12
    private function updateOrderLine(OrderLine $orderLine, ?AbstractProduct $product, Money $pricePerUnit, int $quantity, bool $isCHF, string $type, array $additionalEmails): void
139
    {
140 12
        if ($isCHF) {
141 12
            $balanceCHF = $pricePerUnit->multiply($quantity);
142 12
            $balanceEUR = Money::EUR(0);
143
        } else {
144 1
            $balanceCHF = Money::CHF(0);
145 1
            $balanceEUR = $pricePerUnit->multiply($quantity);
146
        }
147
148 12
        if (!$product) {
149 1
            $orderLine->setDonation();
150 11
        } elseif ($product instanceof Product) {
151 9
            $orderLine->setProduct($product);
152 2
        } elseif ($product instanceof Subscription) {
153 2
            $orderLine->setSubscription($product);
154
        } else {
155
            throw new Exception('Unsupported subclass of product');
156
        }
157
158 12
        $orderLine->setIsCHF($isCHF);
159 12
        $orderLine->setType($type);
160 12
        $orderLine->setQuantity($quantity);
161 12
        $orderLine->setBalanceCHF($balanceCHF);
162 12
        $orderLine->setBalanceEUR($balanceEUR);
163 12
        $orderLine->setAdditionalEmails($additionalEmails);
164
165 12
        $this->createTemporaryUsers($orderLine);
166 12
    }
167
168 12
    private function getPricePerUnit(?AbstractProduct $product, ?float $pricePerUnit, bool $isCHF): Money
169
    {
170 12
        if ($product && $isCHF) {
171 11
            return $product->getPricePerUnitCHF();
172
        }
173
174 2
        if ($product) {
175 1
            return $product->getPricePerUnitEUR();
176
        }
177
178 1
        if ($pricePerUnit === null || $pricePerUnit <= 0) {
179
            throw new Exception('A donation must have strictly positive price');
180
        }
181
182 1
        $pricePerUnit = bcmul((string) $pricePerUnit, '100', 2);
183 1
        if ($isCHF) {
184 1
            return Money::CHF($pricePerUnit);
185
        }
186
187
        return Money::EUR($pricePerUnit);
188
    }
189
190
    /**
191
     * Create temporary users to give them immediate access to web,
192
     * until their access is confirmed permanently via a CSV import
193
     */
194 12
    private function createTemporaryUsers(OrderLine $orderLine): void
195
    {
196 12
        $isDigital = $orderLine->getSubscription() && ProductTypeType::includesDigital($orderLine->getSubscription()->getType());
197
198 12
        foreach ($orderLine->getAdditionalEmails() as $email) {
199 1
            $user = $this->userRepository->getOrCreate($email);
200
201 1
            if ($isDigital) {
202 1
                $user->setWebTemporaryAccess(true);
203
            }
204
        }
205
206 12
        if ($isDigital && $orderLine->getOwner()) {
207 1
            $orderLine->getOwner()->setWebTemporaryAccess(true);
208
        }
209 12
    }
210
}
211