StockMovement::create()   A
last analyzed

Complexity

Conditions 2
Paths 2

Size

Total Lines 23
Code Lines 16

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 23
rs 9.0856
c 0
b 0
f 0
cc 2
eloc 16
nc 2
nop 6
1
<?php
2
3
declare(strict_types = 1);
4
5
namespace Loevgaard\DandomainStock\Entity;
6
7
use Assert\Assert;
8
use Brick\Math\BigDecimal;
9
use Brick\Math\BigInteger;
10
use Doctrine\ORM\Mapping as ORM;
11
use Knp\DoctrineBehaviors\Model\Blameable\Blameable;
12
use Knp\DoctrineBehaviors\Model\Timestampable\Timestampable;
13
use Loevgaard\DandomainFoundation\Entity\Generated\OrderLineInterface;
14
use Loevgaard\DandomainFoundation\Entity\Generated\ProductInterface;
15
use Loevgaard\DandomainStock\Entity\Generated\StockMovementInterface;
16
use Loevgaard\DandomainStock\Entity\Generated\StockMovementTrait;
17
use Loevgaard\DandomainStock\Exception\CurrencyMismatchException;
18
use Loevgaard\DandomainStock\Exception\StockMovementProductMismatchException;
19
use Loevgaard\DandomainStock\Exception\UnsetCurrencyException;
20
use Loevgaard\DandomainStock\Exception\UnsetProductException;
21
use Money\Currency;
22
use Money\Money;
23
use Symfony\Component\Validator\Constraints as FormAssert;
24
25
/**
26
 * @method Money getRetailPriceInclVat()
27
 * @method Money getTotalRetailPriceInclVat()
28
 * @method Money getPriceInclVat()
29
 * @method Money getTotalPriceInclVat()
30
 * @method Money getDiscountInclVat()
31
 * @method Money getTotalDiscountInclVat()
32
 *
33
 * @ORM\Entity()
34
 * @ORM\Table(name="lds_stock_movements", indexes={@ORM\Index(name="type_idx", columns={"`type`"})})
35
 * @ORM\HasLifecycleCallbacks()
36
 **/
37
class StockMovement implements StockMovementInterface
38
{
39
    use StockMovementTrait;
40
    use Blameable;
41
    use Timestampable;
42
43
    const TYPE_SALE = 'sale';
44
    const TYPE_RETURN = 'return';
45
    const TYPE_REGULATION = 'regulation';
46
    const TYPE_DELIVERY = 'delivery';
47
48
    /**
49
     * @var int
50
     *
51
     * @ORM\Column(type="integer")
52
     * @ORM\GeneratedValue
53
     * @ORM\Id
54
     */
55
    protected $id;
56
57
    /**
58
     * The number of items.
59
     *
60
     * If the quantity is negative it means an outgoing stock movement, i.e. you've sold a product
61
     * Contrary a positive number means an ingoing stock movement, i.e. you had a return or a delivery
62
     *
63
     * @var int
64
     *
65
     * @FormAssert\NotBlank()
66
     * @FormAssert\NotEqualTo(0)
67
     *
68
     * @ORM\Column(type="integer")
69
     */
70
    protected $quantity;
71
72
    /**
73
     * Whether this stock movement is a complaint.
74
     *
75
     * @var bool
76
     *
77
     * @ORM\Column(name="complaint", type="boolean")
78
     */
79
    protected $complaint;
80
81
    /**
82
     * A small text describing this stock movement.
83
     *
84
     * @var string
85
     *
86
     * @FormAssert\Length(max="191")
87
     *
88
     * @ORM\Column(name="reference", type="string", length=191)
89
     */
90
    protected $reference;
91
92
    /**
93
     * A valid currency code.
94
     *
95
     * @var string
96
     *
97
     * @FormAssert\Currency()
98
     *
99
     * @ORM\Column(type="string", length=3)
100
     */
101
    protected $currency;
102
103
    /**
104
     * This is the retail price of the product on the time when the stock movement was created
105
     * The price is excl vat.
106
     *
107
     * @var int
108
     *
109
     * @FormAssert\NotBlank()
110
     * @FormAssert\GreaterThanOrEqual(0)
111
     *
112
     * @ORM\Column(type="integer")
113
     */
114
    protected $retailPrice;
115
116
    /**
117
     * Effectively this is `$quantity * $retailPrice`.
118
     *
119
     * The price is excl vat
120
     *
121
     * @var int
122
     *
123
     * @FormAssert\NotBlank()
124
     * @FormAssert\GreaterThanOrEqual(0)
125
     *
126
     * @ORM\Column(type="integer")
127
     */
128
    protected $totalRetailPrice;
129
130
    /**
131
     * This is the price excl vat.
132
     *
133
     * @var int
134
     *
135
     * @FormAssert\NotBlank()
136
     * @FormAssert\GreaterThanOrEqual(0)
137
     *
138
     * @ORM\Column(type="integer")
139
     */
140
    protected $price;
141
142
    /**
143
     * Effectively this is `$quantity * $price`.
144
     *
145
     * This is the total price excl vat
146
     *
147
     * @var int
148
     *
149
     * @FormAssert\NotBlank()
150
     * @FormAssert\GreaterThanOrEqual(0)
151
     *
152
     * @ORM\Column(type="integer")
153
     */
154
    protected $totalPrice;
155
156
    /**
157
     * This is the discount on this stock movement.
158
     *
159
     * Effectively this is `$retailPrice - $price`
160
     *
161
     * @var int
162
     *
163
     * @FormAssert\NotBlank()
164
     *
165
     * @ORM\Column(type="integer")
166
     */
167
    protected $discount;
168
169
    /**
170
     * This is the total discount on this stock movement.
171
     *
172
     * Effectively this is `$totalRetailPrice - $totalPrice`
173
     *
174
     * @var int
175
     *
176
     * @FormAssert\NotBlank()
177
     *
178
     * @ORM\Column(type="integer")
179
     */
180
    protected $totalDiscount;
181
182
    /**
183
     * This is the vat percentage.
184
     *
185
     * @var string
186
     *
187
     * @FormAssert\NotBlank()
188
     * @FormAssert\GreaterThanOrEqual(0)
189
     *
190
     * @ORM\Column(type="decimal", precision=5, scale=2)
191
     */
192
    protected $vatPercentage;
193
194
    /**
195
     * This is the type of the stock movement, i.e. 'sale', 'delivery', 'return' etc.
196
     *
197
     * @var string
198
     *
199
     * @FormAssert\Choice(callback="getTypes")
200
     *
201
     * @ORM\Column(name="`type`", type="string", length=191)
202
     */
203
    protected $type;
204
205
    /**
206
     * This is the associated product.
207
     *
208
     * @var ProductInterface
209
     *
210
     * @FormAssert\NotBlank()
211
     *
212
     * @ORM\JoinColumn(nullable=false)
213
     * @ORM\ManyToOne(targetEntity="Loevgaard\DandomainFoundation\Entity\Product")
214
     */
215
    protected $product;
216
217
    /**
218
     * If the type equals 'sale' this will be the associated order line.
219
     *
220
     * @var OrderLineInterface|null
221
     *
222
     * @ORM\ManyToOne(targetEntity="Loevgaard\DandomainFoundation\Entity\OrderLine", inversedBy="stockMovements")
223
     */
224
    protected $orderLine;
225
226
    /**
227
     * When an associated order line is removed this flag must be set
228
     *
229
     * @var bool
230
     *
231
     * @ORM\Column(type="boolean")
232
     */
233
    protected $orderLineRemoved;
234
235
    public function __construct()
236
    {
237
        $this->complaint = false;
238
        $this->orderLineRemoved = false;
239
    }
240
241
    public function __call($name, $arguments)
242
    {
243
        if ('InclVat' !== substr($name, -7)) {
244
            throw new \InvalidArgumentException('This class only accepts magic calls ending with `InclVat`');
245
        }
246
247
        $method = substr($name, 0, -7);
248
249
        if (false === $method || !method_exists($this, $method)) {
250
            throw new \InvalidArgumentException('The method `'.$method.'` does not exist');
251
        }
252
253
        /** @var Money $val */
254
        $val = $this->{$method}();
255
256
        return $val->multiply($this->getVatMultiplier());
257
    }
258
259
    /**
260
     * @ORM\PrePersist()
261
     * @ORM\PreUpdate()
262
     */
263
    public function validate()
264
    {
265
        Assert::that($this->product)->isInstanceOf(ProductInterface::class);
266
        $identifier = '[P => '.$this->product->getNumber().']';
267
268
        Assert::that($this->type)->choice(self::getTypes());
269
        if ($this->isType(self::TYPE_SALE)) {
270
            if (!$this->isOrderLineRemoved()) {
271
                Assert::that($this->orderLine)->isInstanceOf(OrderLineInterface::class);
272
                $identifier .= '[O => internal: '.$this->orderLine->getOrder()->getId().' | external: '.$this->orderLine->getOrder()->getExternalId().'][OL => internal: '.$this->orderLine->getId().' | external: '.$this->orderLine->getExternalId().']';
273
            }
274
            Assert::that($this->quantity)->lessThan(0, $identifier.' Quantity must be negative when the type equals sale');
275
        } elseif ($this->isType(self::TYPE_RETURN)) {
276
            Assert::that($this->quantity)->greaterThan(0, $identifier.' Quantity should be greater than 0 if the type is a return');
277
        }
278
279
        Assert::that($this->quantity)
280
            ->integer($identifier.' Quantity needs to be an integer', 'quantity')
281
            ->notEq(0, $identifier.' Quantity can never be 0');
282
283
        Assert::that($this->complaint)->boolean();
284
        Assert::thatNullOr($this->reference)->string()->maxLength(191);
285
        Assert::that($this->currency)->string()->length(3);
286
        Assert::that($this->retailPrice)->integer($identifier.' Retail price needs to be an integer', 'retailPrice')->greaterOrEqualThan(0);
287
        Assert::that($this->totalRetailPrice)->integer($identifier.' Total retail price needs to be an integer', 'totalRetailPrice')->greaterOrEqualThan(0);
288
        Assert::that($this->price)->integer($identifier.' Price needs to be an integer', 'price')->greaterOrEqualThan(0);
289
        Assert::that($this->totalPrice)->integer($identifier.' Total price needs to be an integer', 'totalPrice')->greaterOrEqualThan(0);
290
        Assert::that($this->discount)->integer($identifier.' Discount needs to be an integer', 'discount');
291
        Assert::that($this->totalDiscount)->integer($identifier.' Total discount needs to be an integer', 'totalDiscount');
292
        Assert::that($this->vatPercentage)->string($identifier.' Vat percentage needs to be a string', 'vatPercentage');
293
        $castedVatPercentage = intval($this->vatPercentage);
294
        Assert::that($castedVatPercentage)->greaterOrEqualThan(0, 'The vat percentage needs to be greater than 0', 'vatPercentage');
295
296
        if ($this->price > 0) {
297
            // it is assumed that if a product has a price, then it also has a retail price
298
            Assert::that($this->retailPrice)->greaterThan(0, $identifier.' When the price is > 0, then retailPrice also has be > 0');
299
        }
300
301
        if ($this->retailPrice === 0) {
302
            Assert::that($this->discount)->eq(0);
303
        }
304
305
        if ($this->complaint) {
306
            // a complaint will always be a product that you remove from your stock
307
            Assert::that($this->quantity)->lessThan(0, $identifier.' Quantity needs to be negative when the stock movement is a complaint');
308
        }
309
310
        Assert::thatNullOr($this->product->getIsVariantMaster())->false($identifier.' Only simple products and variants is allowed as stock movements');
311
    }
312
313
    /**
314
     * @param int              $quantity
315
     * @param Money            $unitPrice
316
     * @param float|string     $vatPercent
317
     * @param string           $type
318
     * @param ProductInterface $product
319
     * @param string           $reference
320
     *
321
     * @return StockMovementInterface
322
     *
323
     * @throws \Loevgaard\DandomainStock\Exception\CurrencyMismatchException
324
     */
325
    public static function create(int $quantity, Money $unitPrice, $vatPercent, string $type, ProductInterface $product, string $reference): StockMovementInterface
326
    {
327
        $stockMovement = new StockMovement();
328
        $stockMovement
329
            ->setQuantity($quantity)
330
            ->setPrice($unitPrice)
331
            ->setVatPercentage((string)$vatPercent)
332
            ->setType($type)
333
            ->setProduct($product)
334
            ->setReference($reference)
335
        ;
336
337
        $retailPrice = $product->findPriceByCurrency($unitPrice->getCurrency());
338
        if ($retailPrice) {
339
            $retailPrice = $retailPrice->getUnitPriceExclVat((float)$vatPercent);
340
        } else {
341
            $retailPrice = $unitPrice;
342
        }
343
344
        $stockMovement->setRetailPrice($retailPrice);
345
346
        return $stockMovement;
347
    }
348
349
    /**
350
     * @param OrderLineInterface $orderLine
351
     *
352
     * @throws \Loevgaard\DandomainStock\Exception\CurrencyMismatchException
353
     * @throws \Loevgaard\DandomainStock\Exception\UnsetProductException
354
     */
355
    public function populateFromOrderLine(OrderLineInterface $orderLine)
356
    {
357
        if (!$orderLine->getProduct()) {
358
            throw new UnsetProductException('No product set on order line with product number: '.$orderLine->getProductNumber());
359
        }
360
361
        $created = new \DateTime($orderLine->getOrder()->getCreatedDate()->format(\DateTime::ATOM));
362
363
        $this
364
            ->setQuantity(-1 * $orderLine->getQuantity()) // we multiply by -1 because we count an order as 'outgoing' from the stock
365
            ->setPrice($orderLine->getUnitPriceExclVat())
366
            ->setVatPercentage((string)$orderLine->getVatPct())
367
            ->setType(static::TYPE_SALE)
368
            ->setProduct($orderLine->getProduct())
369
            ->setOrderLine($orderLine)
370
            ->setReference('Order '.$orderLine->getOrder()->getExternalId())
371
            ->setCreatedAt($created) // for order lines we specifically override the createdAt and updatedAt dates because the stock movement is actually happening when the order comes in and not when the order is synced
372
        ;
373
374
        $retailPrice = $orderLine->getProduct()->findPriceByCurrency($orderLine->getUnitPrice()->getCurrency());
375
        if ($retailPrice) {
376
            $retailPrice = $retailPrice->getUnitPriceExclVat($orderLine->getVatPct());
377
        } else {
378
            $retailPrice = $orderLine->getUnitPriceExclVat();
379
        }
380
381
        $this->setRetailPrice($retailPrice);
382
    }
383
384
    /**
385
     * @return StockMovement
386
     *
387
     * @throws \Loevgaard\DandomainStock\Exception\CurrencyMismatchException
388
     * @throws \Loevgaard\DandomainStock\Exception\UnsetCurrencyException
389
     */
390
    public function copy(): self
391
    {
392
        $stockMovement = new static();
393
        $stockMovement
394
            ->setQuantity($this->getQuantity())
395
            ->setComplaint($this->isComplaint())
396
            ->setReference($this->getReference())
397
            ->setRetailPrice($this->getRetailPrice())
398
            ->setPrice($this->getPrice())
399
            ->setVatPercentage($this->getVatPercentage())
400
            ->setType($this->getType())
401
            ->setProduct($this->getProduct())
402
            ->setOrderLine($this->getOrderLine())
403
            ->setOrderLineRemoved($this->isOrderLineRemoved())
404
        ;
405
406
        return $stockMovement;
407
    }
408
409
    /**
410
     * @return StockMovementInterface
411
     *
412
     * @throws \Loevgaard\DandomainStock\Exception\CurrencyMismatchException
413
     * @throws \Loevgaard\DandomainStock\Exception\UnsetCurrencyException
414
     */
415
    public function inverse(): StockMovementInterface
416
    {
417
        $stockMovement = $this->copy();
418
        $stockMovement->setQuantity($stockMovement->getQuantity() * -1);
419
420
        return $stockMovement;
421
    }
422
423
    /**
424
     * @param StockMovementInterface $stockMovement
425
     * @return StockMovementInterface
426
     * @throws \Loevgaard\DandomainStock\Exception\CurrencyMismatchException
427
     * @throws \Loevgaard\DandomainStock\Exception\UnsetCurrencyException
428
     * @throws \Loevgaard\DandomainStock\Exception\StockMovementProductMismatchException
429
     */
430
    public function diff(StockMovementInterface $stockMovement): StockMovementInterface
431
    {
432
        if ($this->getProduct()->getId() !== $stockMovement->getProduct()->getId()) {
433
            throw new StockMovementProductMismatchException('Can only compute diff between stock movements where the products equal');
434
        }
435
436
        $qty = -1 * ($this->getQuantity() - $stockMovement->getQuantity());
437
438
        $diff = $stockMovement->copy();
439
        $diff->setQuantity($qty);
440
441
        return $diff;
442
    }
443
444
    /**
445
     * Returns the valid types.
446
     *
447
     * @return array
448
     */
449
    public static function getTypes(): array
450
    {
451
        return [
452
            self::TYPE_DELIVERY => self::TYPE_DELIVERY,
453
            self::TYPE_SALE => self::TYPE_SALE,
454
            self::TYPE_REGULATION => self::TYPE_REGULATION,
455
            self::TYPE_RETURN => self::TYPE_RETURN,
456
        ];
457
    }
458
459
    /**
460
     * Returns true if $type equals the type of the stock movement.
461
     *
462
     * @param string $type
463
     *
464
     * @return bool
465
     */
466
    public function isType(string $type): bool
467
    {
468
        return $this->type === $type;
469
    }
470
471
    /*********************
472
     * Getters / Setters *
473
     ********************/
474
475
    /**
476
     * @return int
477
     */
478
    public function getId(): int
479
    {
480
        return (int) $this->id;
481
    }
482
483
    /**
484
     * @param int $id
485
     *
486
     * @return StockMovement
487
     */
488
    public function setId(int $id): self
489
    {
490
        $this->id = $id;
491
492
        return $this;
493
    }
494
495
    /**
496
     * @return int
497
     */
498
    public function getQuantity(): int
499
    {
500
        return (int) $this->quantity;
501
    }
502
503
    /**
504
     * @param int $quantity
505
     *
506
     * @return StockMovement
507
     */
508
    public function setQuantity(int $quantity): self
509
    {
510
        $this->quantity = $quantity;
511
        $this->updateTotalPrice();
512
        $this->updateTotalRetailPrice();
513
514
        return $this;
515
    }
516
517
    /**
518
     * @return bool
519
     */
520
    public function isComplaint(): bool
521
    {
522
        return (bool) $this->complaint;
523
    }
524
525
    /**
526
     * @param bool $complaint
527
     *
528
     * @return StockMovement
529
     */
530
    public function setComplaint(bool $complaint): self
531
    {
532
        $this->complaint = $complaint;
533
534
        return $this;
535
    }
536
537
    /**
538
     * @return string
539
     */
540
    public function getReference(): string
541
    {
542
        return (string) $this->reference;
543
    }
544
545
    /**
546
     * @param string $reference
547
     *
548
     * @return StockMovement
549
     */
550
    public function setReference(string $reference): self
551
    {
552
        $this->reference = $reference;
553
554
        return $this;
555
    }
556
557
    /**
558
     * @return string
559
     */
560
    public function getCurrency(): string
561
    {
562
        return (string) $this->currency;
563
    }
564
565
    /**
566
     * @return Money
567
     *
568
     * @throws \Loevgaard\DandomainStock\Exception\UnsetCurrencyException
569
     */
570
    public function getRetailPrice(): Money
571
    {
572
        return $this->money((int) $this->retailPrice);
573
    }
574
575
    /**
576
     * @param Money $retailPrice
577
     *
578
     * @return $this
579
     *
580
     * @throws \Loevgaard\DandomainStock\Exception\CurrencyMismatchException
581
     */
582
    public function setRetailPrice(Money $retailPrice): self
583
    {
584
        $this->updateCurrency($retailPrice);
585
        $this->retailPrice = (int) $retailPrice->getAmount();
586
        $this->updateTotalRetailPrice();
587
588
        return $this;
589
    }
590
591
    /**
592
     * @return Money
593
     *
594
     * @throws \Loevgaard\DandomainStock\Exception\UnsetCurrencyException
595
     */
596
    public function getTotalRetailPrice(): Money
597
    {
598
        return $this->money((int) $this->totalRetailPrice);
599
    }
600
601
    /**
602
     * @return Money
603
     *
604
     * @throws \Loevgaard\DandomainStock\Exception\UnsetCurrencyException
605
     */
606
    public function getPrice(): Money
607
    {
608
        return $this->money((int) $this->price);
609
    }
610
611
    /**
612
     * @param Money $price
613
     *
614
     * @return $this
615
     *
616
     * @throws \Loevgaard\DandomainStock\Exception\CurrencyMismatchException
617
     */
618
    public function setPrice(Money $price): self
619
    {
620
        $this->updateCurrency($price);
621
        $this->price = (int) $price->getAmount();
622
        $this->updateTotalPrice();
623
624
        return $this;
625
    }
626
627
    /**
628
     * @return Money
629
     *
630
     * @throws \Loevgaard\DandomainStock\Exception\UnsetCurrencyException
631
     */
632
    public function getTotalPrice(): Money
633
    {
634
        return $this->money((int) $this->totalPrice);
635
    }
636
637
    /**
638
     * @return Money
639
     *
640
     * @throws \Loevgaard\DandomainStock\Exception\UnsetCurrencyException
641
     */
642
    public function getDiscount(): Money
643
    {
644
        return $this->money((int) $this->discount);
645
    }
646
647
    /**
648
     * @return Money
649
     *
650
     * @throws \Loevgaard\DandomainStock\Exception\UnsetCurrencyException
651
     */
652
    public function getTotalDiscount(): Money
653
    {
654
        return $this->money((int) $this->totalDiscount);
655
    }
656
657
    /**
658
     * @return string
659
     */
660
    public function getVatPercentage(): string
661
    {
662
        return (string) $this->vatPercentage;
663
    }
664
665
    /**
666
     * @param string $vatPercentage
667
     *
668
     * @return StockMovement
669
     */
670
    public function setVatPercentage(string $vatPercentage): self
671
    {
672
        $this->vatPercentage = $vatPercentage;
673
674
        return $this;
675
    }
676
677
    /**
678
     * @return string
679
     */
680
    public function getType(): string
681
    {
682
        return (string) $this->type;
683
    }
684
685
    /**
686
     * @param string $type
687
     *
688
     * @return StockMovement
689
     */
690
    public function setType(string $type): self
691
    {
692
        $this->type = $type;
693
694
        return $this;
695
    }
696
697
    /**
698
     * @return ProductInterface
699
     */
700
    public function getProduct(): ?ProductInterface
701
    {
702
        return $this->product;
703
    }
704
705
    /**
706
     * @param ProductInterface $product
707
     *
708
     * @return StockMovement
709
     */
710
    public function setProduct(ProductInterface $product): self
711
    {
712
        $this->product = $product;
713
714
        return $this;
715
    }
716
717
    /**
718
     * @return OrderLineInterface|null
719
     */
720
    public function getOrderLine(): ?OrderLineInterface
721
    {
722
        return $this->orderLine;
723
    }
724
725
    /**
726
     * @param OrderLineInterface|null $orderLine
727
     *
728
     * @return StockMovement
729
     */
730
    public function setOrderLine(?OrderLineInterface $orderLine): self
731
    {
732
        $this->orderLine = $orderLine;
733
734
        return $this;
735
    }
736
737
    /**
738
     * @return bool
739
     */
740
    public function isOrderLineRemoved(): bool
741
    {
742
        return (bool) $this->orderLineRemoved;
743
    }
744
745
    /**
746
     * @param bool $orderLineRemoved
747
     *
748
     * @return StockMovement
749
     */
750
    public function setOrderLineRemoved(bool $orderLineRemoved)
751
    {
752
        $this->orderLineRemoved = $orderLineRemoved;
753
754
        return $this;
755
    }
756
757
    /****************************
758
     * Protected helper methods *
759
     ***************************/
760
761
    protected function updateTotalPrice(): void
762
    {
763
        if (is_int($this->price) && is_int($this->quantity)) {
764
            $this->totalPrice = BigInteger::of($this->price)->multipliedBy(abs($this->quantity))->toInt();
765
766
            $this->updateDiscount();
767
        }
768
    }
769
770
    protected function updateTotalRetailPrice(): void
771
    {
772
        if (is_int($this->retailPrice) && is_int($this->quantity)) {
773
            $this->totalRetailPrice = BigInteger::of($this->retailPrice)->multipliedBy(abs($this->quantity))->toInt();
774
775
            $this->updateDiscount();
776
        }
777
    }
778
779
    protected function updateDiscount(): void
780
    {
781
        if (is_int($this->retailPrice) && is_int($this->totalRetailPrice) && is_int($this->price) && is_int($this->totalPrice)) {
782
            $this->discount = $this->retailPrice - $this->price;
783
            $this->totalDiscount = $this->totalRetailPrice - $this->totalPrice;
784
        }
785
    }
786
787
    /**
788
     * Updates the shared currency.
789
     *
790
     * If the currency is already set and the new currency is not the same, it throws an exception
791
     *
792
     * @param Money $money
793
     *
794
     * @return StockMovement
795
     *
796
     * @throws \Loevgaard\DandomainStock\Exception\CurrencyMismatchException
797
     */
798
    protected function updateCurrency(Money $money): self
799
    {
800
        if ($this->currency && $money->getCurrency()->getCode() !== $this->currency) {
801
            throw new CurrencyMismatchException('The currency on this stock movement is not the same as the one your Money object');
802
        }
803
804
        $this->currency = $money->getCurrency()->getCode();
805
806
        return $this;
807
    }
808
809
    /**
810
     * Returns a new Money object based on the shared currency
811
     * If no currency is set, it throws an exception.
812
     *
813
     * @param int $val
814
     *
815
     * @return Money
816
     *
817
     * @throws \Loevgaard\DandomainStock\Exception\UnsetCurrencyException
818
     */
819
    protected function money(int $val): Money
820
    {
821
        if (!$this->currency) {
822
            throw new UnsetCurrencyException('The currency is not set on this stock movement');
823
        }
824
825
        return new Money($val, new Currency($this->currency));
826
    }
827
828
    /**
829
     * Returns a vat multiplier for this stock movement.
830
     *
831
     * Example: You have a vat percentage of (float)25.0 then this method will return (string)1.25
832
     *
833
     * @return string
834
     */
835
    protected function getVatMultiplier(): string
836
    {
837
        return (string) BigDecimal::of('100')->plus($this->getVatPercentage())->exactlyDividedBy(100);
838
    }
839
}
840