1 | <?php |
||||
2 | |||||
3 | declare(strict_types=1); |
||||
4 | |||||
5 | namespace ApplicationTest\Service; |
||||
6 | |||||
7 | use Application\Enum\PaymentMethod; |
||||
8 | use Application\Enum\ProductType; |
||||
9 | use Application\Model\AbstractProduct; |
||||
10 | use Application\Model\Order; |
||||
11 | use Application\Model\OrderLine; |
||||
12 | use Application\Model\Product; |
||||
13 | use Application\Model\Subscription; |
||||
14 | use Application\Service\Invoicer; |
||||
15 | use ApplicationTest\Traits\TestWithTransactionAndUser; |
||||
16 | use Money\Money; |
||||
17 | use PHPUnit\Framework\TestCase; |
||||
18 | |||||
19 | class InvoicerTest extends TestCase |
||||
20 | { |
||||
21 | use TestWithTransactionAndUser; |
||||
22 | |||||
23 | /** |
||||
24 | * @dataProvider providerCreateOrder |
||||
25 | */ |
||||
26 | public function testCreateOrder(array $input, array $expectedOrderLines): void |
||||
27 | { |
||||
28 | $input['orderLines'] = $this->hydrateTestData($input['orderLines']); |
||||
29 | |||||
30 | global $container; |
||||
31 | /** @var Invoicer $invoicer */ |
||||
32 | $invoicer = $container->get(Invoicer::class); |
||||
33 | $order = $invoicer->createOrder($input); |
||||
34 | |||||
35 | $actualOrderLines = $this->extractOrderLines($order); |
||||
0 ignored issues
–
show
Bug
introduced
by
![]() |
|||||
36 | self::assertSame($expectedOrderLines, $actualOrderLines); |
||||
37 | } |
||||
38 | |||||
39 | /** |
||||
40 | * @dataProvider providerUpdateOrderLineAndTransactionLine |
||||
41 | */ |
||||
42 | public function testUpdateOrderLineAndTransactionLine(string $originalOrder, ?array $newProduct, array $expectedOrderLines): void |
||||
43 | { |
||||
44 | $input = $this->providerCreateOrder()[$originalOrder][0]; |
||||
45 | $input['orderLines'] = $this->hydrateTestData($input['orderLines']); |
||||
46 | |||||
47 | global $container; |
||||
48 | /** @var Invoicer $invoicer */ |
||||
49 | $invoicer = $container->get(Invoicer::class); |
||||
50 | $order = $invoicer->createOrder($input); |
||||
51 | |||||
52 | if ($newProduct) { |
||||
53 | $product = $this->hydrateProduct($newProduct); |
||||
54 | } else { |
||||
55 | $product = $input['orderLines'][0]['product']; |
||||
56 | } |
||||
57 | |||||
58 | $line = [ |
||||
59 | 'quantity' => 100, |
||||
60 | 'isCHF' => true, |
||||
61 | 'type' => ProductType::Digital, |
||||
62 | 'product' => $product, |
||||
63 | 'additionalEmails' => [], |
||||
64 | ]; |
||||
65 | |||||
66 | $invoicer->updateOrderLineAndTransactionLine($order->getOrderLines()->first(), $line); |
||||
67 | |||||
68 | $actualOrderLines = $this->extractOrderLines($order); |
||||
0 ignored issues
–
show
It seems like
$order can also be of type null ; however, parameter $order of ApplicationTest\Service\...st::extractOrderLines() does only seem to accept Application\Model\Order , 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
![]() |
|||||
69 | self::assertSame($expectedOrderLines, $actualOrderLines); |
||||
70 | } |
||||
71 | |||||
72 | public function providerUpdateOrderLineAndTransactionLine(): iterable |
||||
73 | { |
||||
74 | yield 'more quantity of same product' => [ |
||||
75 | 'normal', |
||||
76 | null, |
||||
77 | [ |
||||
78 | [ |
||||
79 | 'My product 1', |
||||
80 | 100, |
||||
81 | '27500', |
||||
82 | '0', |
||||
83 | true, |
||||
84 | ProductType::Digital, |
||||
85 | ], |
||||
86 | [ |
||||
87 | 'My product 2', |
||||
88 | 1, |
||||
89 | '20000', |
||||
90 | '0', |
||||
91 | true, |
||||
92 | ProductType::Digital, |
||||
93 | ], |
||||
94 | ], |
||||
95 | ]; |
||||
96 | yield 'more quantity of different, negative product' => [ |
||||
97 | 'normal', |
||||
98 | [ |
||||
99 | 'name' => 'My negative product', |
||||
100 | 'pricePerUnitCHF' => Money::CHF(-10000), |
||||
101 | 'pricePerUnitEUR' => Money::EUR(-15000), |
||||
102 | ], |
||||
103 | [ |
||||
104 | [ |
||||
105 | 'My negative product', |
||||
106 | 100, |
||||
107 | '-1000000', |
||||
108 | '0', |
||||
109 | true, |
||||
110 | ProductType::Digital, |
||||
111 | ], |
||||
112 | [ |
||||
113 | 'My product 2', |
||||
114 | 1, |
||||
115 | '20000', |
||||
116 | '0', |
||||
117 | true, |
||||
118 | ProductType::Digital, |
||||
119 | ], |
||||
120 | ], |
||||
121 | ]; |
||||
122 | yield 'from negative goes back to positive' => [ |
||||
123 | 'negative balance should swap accounts', |
||||
124 | [ |
||||
125 | 'name' => 'My positive product', |
||||
126 | 'pricePerUnitCHF' => Money::CHF(10000), |
||||
127 | 'pricePerUnitEUR' => Money::EUR(15000), |
||||
128 | ], |
||||
129 | [ |
||||
130 | [ |
||||
131 | 'My positive product', |
||||
132 | 100, |
||||
133 | '1000000', |
||||
134 | '0', |
||||
135 | true, |
||||
136 | ProductType::Digital, |
||||
137 | ], |
||||
138 | ], |
||||
139 | ]; |
||||
140 | } |
||||
141 | |||||
142 | public function providerCreateOrder(): array |
||||
143 | { |
||||
144 | return [ |
||||
145 | 'free product should create order, even with transactions for zero dollars' => [ |
||||
146 | [ |
||||
147 | 'paymentMethod' => PaymentMethod::Bvr, |
||||
148 | 'orderLines' => [ |
||||
149 | [ |
||||
150 | 'quantity' => 1, |
||||
151 | 'isCHF' => true, |
||||
152 | 'type' => ProductType::Digital, |
||||
153 | 'product' => [ |
||||
154 | 'name' => 'My product', |
||||
155 | 'pricePerUnitCHF' => Money::CHF(0), |
||||
156 | 'pricePerUnitEUR' => Money::EUR(0), |
||||
157 | ], |
||||
158 | 'additionalEmails' => [], |
||||
159 | ], |
||||
160 | ], |
||||
161 | ], |
||||
162 | [ |
||||
163 | [ |
||||
164 | 'My product', |
||||
165 | 1, |
||||
166 | '0', |
||||
167 | '0', |
||||
168 | true, |
||||
169 | ProductType::Digital, |
||||
170 | ], |
||||
171 | ], |
||||
172 | ], |
||||
173 | 'normal' => [ |
||||
174 | [ |
||||
175 | 'paymentMethod' => PaymentMethod::Bvr, |
||||
176 | 'orderLines' => [ |
||||
177 | [ |
||||
178 | 'quantity' => 3, |
||||
179 | 'isCHF' => true, |
||||
180 | 'type' => ProductType::Digital, |
||||
181 | 'product' => [ |
||||
182 | 'name' => 'My product 1', |
||||
183 | 'pricePerUnitCHF' => Money::CHF(275), |
||||
184 | 'pricePerUnitEUR' => Money::EUR(280), |
||||
185 | ], |
||||
186 | 'additionalEmails' => [], |
||||
187 | ], |
||||
188 | [ |
||||
189 | 'quantity' => 1, |
||||
190 | 'isCHF' => true, |
||||
191 | 'type' => ProductType::Digital, |
||||
192 | 'product' => [ |
||||
193 | 'name' => 'My product 2', |
||||
194 | 'pricePerUnitCHF' => Money::CHF(20000), |
||||
195 | 'pricePerUnitEUR' => Money::EUR(25000), |
||||
196 | ], |
||||
197 | 'additionalEmails' => [], |
||||
198 | ], |
||||
199 | ], |
||||
200 | ], |
||||
201 | [ |
||||
202 | [ |
||||
203 | 'My product 1', |
||||
204 | 3, |
||||
205 | '825', |
||||
206 | '0', |
||||
207 | true, |
||||
208 | ProductType::Digital, |
||||
209 | ], |
||||
210 | [ |
||||
211 | 'My product 2', |
||||
212 | 1, |
||||
213 | '20000', |
||||
214 | '0', |
||||
215 | true, |
||||
216 | ProductType::Digital, |
||||
217 | ], |
||||
218 | ], |
||||
219 | ], |
||||
220 | 'with mixed CHF/EURO prices' => [ |
||||
221 | [ |
||||
222 | 'paymentMethod' => PaymentMethod::Bvr, |
||||
223 | 'orderLines' => [ |
||||
224 | [ |
||||
225 | 'quantity' => 3, |
||||
226 | 'isCHF' => false, |
||||
227 | 'type' => ProductType::Digital, |
||||
228 | 'product' => [ |
||||
229 | 'name' => 'My product 1', |
||||
230 | 'pricePerUnitCHF' => Money::CHF(275), |
||||
231 | 'pricePerUnitEUR' => Money::EUR(280), |
||||
232 | ], |
||||
233 | 'additionalEmails' => [], |
||||
234 | ], |
||||
235 | [ |
||||
236 | 'quantity' => 1, |
||||
237 | 'isCHF' => true, |
||||
238 | 'type' => ProductType::Paper, |
||||
239 | 'product' => [ |
||||
240 | 'name' => 'My product 2', |
||||
241 | 'pricePerUnitCHF' => Money::CHF(20000), |
||||
242 | 'pricePerUnitEUR' => Money::EUR(25000), |
||||
243 | ], |
||||
244 | 'additionalEmails' => [], |
||||
245 | ], |
||||
246 | ], |
||||
247 | ], |
||||
248 | [ |
||||
249 | [ |
||||
250 | 'My product 1', |
||||
251 | 3, |
||||
252 | '0', |
||||
253 | '840', |
||||
254 | false, |
||||
255 | ProductType::Digital, |
||||
256 | ], |
||||
257 | [ |
||||
258 | 'My product 2', |
||||
259 | 1, |
||||
260 | '20000', |
||||
261 | '0', |
||||
262 | true, |
||||
263 | ProductType::Paper, |
||||
264 | ], |
||||
265 | ], |
||||
266 | ], |
||||
267 | 'negative balance should swap accounts' => [ |
||||
268 | [ |
||||
269 | 'paymentMethod' => PaymentMethod::Bvr, |
||||
270 | 'orderLines' => [ |
||||
271 | [ |
||||
272 | 'quantity' => 1, |
||||
273 | 'isCHF' => true, |
||||
274 | 'type' => ProductType::Digital, |
||||
275 | 'product' => [ |
||||
276 | 'name' => 'My product', |
||||
277 | 'pricePerUnitCHF' => Money::CHF(-10000), |
||||
278 | 'pricePerUnitEUR' => Money::EUR(-15000), |
||||
279 | ], |
||||
280 | 'additionalEmails' => [], |
||||
281 | ], |
||||
282 | ], |
||||
283 | ], |
||||
284 | [ |
||||
285 | [ |
||||
286 | 'My product', |
||||
287 | 1, |
||||
288 | '-10000', |
||||
289 | '0', |
||||
290 | true, |
||||
291 | ProductType::Digital, |
||||
292 | ], |
||||
293 | ], |
||||
294 | ], |
||||
295 | 'can create order for subscription' => [ |
||||
296 | [ |
||||
297 | 'paymentMethod' => PaymentMethod::Bvr, |
||||
298 | 'orderLines' => [ |
||||
299 | [ |
||||
300 | 'quantity' => 1, |
||||
301 | 'isCHF' => true, |
||||
302 | 'type' => ProductType::Digital, |
||||
303 | 'subscription' => [ |
||||
304 | 'name' => 'My subscription', |
||||
305 | 'pricePerUnitCHF' => Money::CHF(10000), |
||||
306 | 'pricePerUnitEUR' => Money::EUR(15000), |
||||
307 | 'type' => ProductType::Both, |
||||
308 | ], |
||||
309 | 'additionalEmails' => [], |
||||
310 | ], |
||||
311 | ], |
||||
312 | ], |
||||
313 | [ |
||||
314 | [ |
||||
315 | 'My subscription', |
||||
316 | 1, |
||||
317 | '10000', |
||||
318 | '0', |
||||
319 | true, |
||||
320 | ProductType::Both, |
||||
321 | ], |
||||
322 | ], |
||||
323 | ], |
||||
324 | 'can create order for donation' => [ |
||||
325 | [ |
||||
326 | 'paymentMethod' => PaymentMethod::Bvr, |
||||
327 | 'orderLines' => [ |
||||
328 | [ |
||||
329 | 'quantity' => 1, |
||||
330 | 'isCHF' => true, |
||||
331 | 'type' => ProductType::Digital, |
||||
332 | 'pricePerUnit' => Money::CHF(10000), |
||||
333 | 'additionalEmails' => [], |
||||
334 | ], |
||||
335 | ], |
||||
336 | ], |
||||
337 | [ |
||||
338 | [ |
||||
339 | 'Don', |
||||
340 | 1, |
||||
341 | '10000', |
||||
342 | '0', |
||||
343 | true, |
||||
344 | ProductType::Digital, |
||||
345 | ], |
||||
346 | ], |
||||
347 | ], |
||||
348 | ]; |
||||
349 | } |
||||
350 | |||||
351 | private function hydrateTestData(array $input): array |
||||
352 | { |
||||
353 | foreach ($input as &$i) { |
||||
354 | if (array_key_exists('product', $i)) { |
||||
355 | $i['product'] = $this->hydrateProduct($i['product']); |
||||
356 | } |
||||
357 | if (array_key_exists('subscription', $i)) { |
||||
358 | $i['subscription'] = $this->hydrateSubscription($i['subscription']); |
||||
359 | } |
||||
360 | } |
||||
361 | |||||
362 | return $input; |
||||
363 | } |
||||
364 | |||||
365 | private function extractOrderLines(Order $order): array |
||||
366 | { |
||||
367 | $actualOrderLines = []; |
||||
368 | /** @var OrderLine $orderLine */ |
||||
369 | foreach ($order->getOrderLines() as $orderLine) { |
||||
370 | $abstractProduct = $orderLine->getProduct() ?: $orderLine->getSubscription(); |
||||
371 | $expectedName = $abstractProduct ? $abstractProduct->getName() : 'Don'; |
||||
372 | self::assertSame($expectedName, $orderLine->getName()); |
||||
373 | |||||
374 | $actualOrderLines[] = [ |
||||
375 | $orderLine->getName(), |
||||
376 | $orderLine->getQuantity(), |
||||
377 | $orderLine->getBalanceCHF()->getAmount(), |
||||
378 | $orderLine->getBalanceEUR()->getAmount(), |
||||
379 | $orderLine->isCHF(), |
||||
380 | $orderLine->getType(), |
||||
381 | ]; |
||||
382 | } |
||||
383 | |||||
384 | return $actualOrderLines; |
||||
385 | } |
||||
386 | |||||
387 | private function hydrateProduct(array $p): Product |
||||
388 | { |
||||
389 | $product = new Product(); |
||||
390 | $this->hydrateAbstractProduct($product, $p); |
||||
391 | |||||
392 | return $product; |
||||
393 | } |
||||
394 | |||||
395 | private function hydrateSubscription(array $s): Subscription |
||||
396 | { |
||||
397 | $subscription = new Subscription(); |
||||
398 | $this->hydrateAbstractProduct($subscription, $s); |
||||
399 | |||||
400 | return $subscription; |
||||
401 | } |
||||
402 | |||||
403 | private function hydrateAbstractProduct(AbstractProduct $product, array $p): void |
||||
404 | { |
||||
405 | $product->setName($p['name']); |
||||
406 | $product->setPricePerUnitCHF($p['pricePerUnitCHF']); |
||||
407 | $product->setPricePerUnitEUR($p['pricePerUnitEUR']); |
||||
408 | $product->setType($p['type'] ?? ProductType::Digital); |
||||
409 | } |
||||
410 | } |
||||
411 |