Completed
Push — master ( e83455...d2af9f )
by Andrii
06:07 queued 04:16
created

Discount   A

Complexity

Total Complexity 40

Size/Duplication

Total Lines 155
Duplicated Lines 0 %

Test Coverage

Coverage 76%

Importance

Changes 0
Metric Value
eloc 69
dl 0
loc 155
ccs 57
cts 75
cp 0.76
rs 9.2
c 0
b 0
f 0
wmc 40

12 Methods

Rating   Name   Duplication   Size   Complexity  
A add() 0 14 3
A multiply() 0 7 3
A __construct() 0 4 1
A isRelative() 0 3 1
C ensureValidValue() 0 37 12
A getValue() 0 3 2
A calculateCompensatedQuantity() 0 11 4
A calculateSum() 0 17 4
A ensureSameType() 0 7 5
A compare() 0 11 3
A isPercentPoint() 0 3 1
A isAbsolute() 0 3 1

How to fix   Complexity   

Complex Class

Complex classes like Discount often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use Discount, and based on these observations, apply Extract Interface, too.

1
<?php
2
/**
3
 * PHP Billing Library
4
 *
5
 * @link      https://github.com/hiqdev/php-billing
6
 * @package   php-billing
7
 * @license   BSD-3-Clause
8
 * @copyright Copyright (c) 2017-2018, HiQDev (http://hiqdev.com/)
9
 */
10
11
namespace hiqdev\php\billing\charge\modifiers\addons;
12
13
use hiqdev\php\billing\charge\ChargeInterface;
14
use hiqdev\php\billing\charge\modifiers\AddonInterface;
15
use hiqdev\php\billing\charge\modifiers\PercentPoint;
16
use hiqdev\php\billing\formula\FormulaSemanticsError;
17
use hiqdev\php\billing\price\SinglePrice;
18
use hiqdev\php\units\Quantity;
19
use hiqdev\php\units\QuantityInterface;
20
use Money\Currencies\ISOCurrencies;
21
use Money\Currency;
22
use Money\Money;
23
use Money\Parser\DecimalMoneyParser;
24
25
/**
26
 * Discount addon.
27
 *
28
 * @author Andrii Vasyliev <[email protected]>
29
 */
30
class Discount implements AddonInterface
31
{
32
    protected static $name = 'discount';
33
34
    /**
35
     * @var string|Money
36
     */
37
    protected $value;
38
39
    protected $moneyParser;
40
41 26
    public function __construct($value)
42
    {
43 26
        $this->moneyParser = new DecimalMoneyParser(new ISOCurrencies());
44 26
        $this->value = $this->ensureValidValue($value);
0 ignored issues
show
Documentation Bug introduced by
It seems like $this->ensureValidValue($value) can also be of type hiqdev\php\billing\charge\modifiers\PercentPoint or hiqdev\php\units\Quantity. However, the property $value is declared as type Money\Money|string. Maybe add an additional type check?

Our type inference engine has found a suspicous assignment of a value to a property. This check raises an issue when a value that can be of a mixed type is assigned to a property that is type hinted more strictly.

For example, imagine you have a variable $accountId that can either hold an Id object or false (if there is no account id yet). Your code now assigns that value to the id property of an instance of the Account class. This class holds a proper account, so the id value must no longer be false.

Either this assignment is in error or a type check should be added for that assignment.

class Id
{
    public $id;

    public function __construct($id)
    {
        $this->id = $id;
    }

}

class Account
{
    /** @var  Id $id */
    public $id;
}

$account_id = false;

if (starsAreRight()) {
    $account_id = new Id(42);
}

$account = new Account();
if ($account instanceof Id)
{
    $account->id = $account_id;
}
Loading history...
45 26
    }
46
47 11
    public function getValue()
48
    {
49 11
        return $this->value instanceof PercentPoint ? $this->value->getNumber() : $this->value;
0 ignored issues
show
introduced by
$this->value is never a sub-type of hiqdev\php\billing\charge\modifiers\PercentPoint.
Loading history...
50
    }
51
52 12
    public function isAbsolute()
53
    {
54 12
        return $this->value instanceof Money;
55
    }
56
57 6
    public function isRelative()
58
    {
59 6
        return !$this->isAbsolute();
60
    }
61
62 2
    public function isPercentPoint(): bool
63
    {
64 2
        return $this->value instanceof PercentPoint;
65
    }
66
67 26
    public function ensureValidValue($value)
68
    {
69 26
        if ($value instanceof self) {
70
            return $value->getValue();
71
        }
72
73 26
        if ($value instanceof Money || $value instanceof PercentPoint || $value instanceof Quantity) {
74 8
            return $value;
75
        }
76
77 25
        if (is_numeric($value)) {
78 10
            return (string)$value;
79
        }
80
81 22
        $name = static::$name;
82 22
        if (\is_string($value)) {
83 22
            if (preg_match('/^(\d{1,5}(\.\d+)?)(%|pp| [A-Z]{3})$/', $value, $ms)) {
84 22
                if ($ms[3] === '%') {
85 20
                    return $ms[1];
86
                }
87 16
                if ($ms[3] === 'pp') {
88
                    return new PercentPoint($ms[1]);
89
                }
90
91 16
                return $this->moneyParser->parse($ms[1], new Currency(trim($ms[3])));
92
            }
93
94 4
            if (preg_match('/^(\d{1,5}(\.\d+)?) ([a-z]{2,})$/', $value, $ms)) {
95
                try {
96
                    return Quantity::create($ms[3], $ms[1]);
97
                } catch (\Exception $e) {
98
                    throw new FormulaSemanticsError("invalid $name value: $value");
99
                }
100
            }
101
        }
102
103 4
        throw new FormulaSemanticsError("invalid $name value: $value");
104
    }
105
106 6
    public function multiply($multiplier)
107
    {
108 6
        if (!is_numeric($multiplier)) {
109 3
            throw new FormulaSemanticsError('multiplier for discount must be numeric');
110
        }
111
112 3
        return new static($this->isAbsolute() ? $this->value->multiply($multiplier) : $this->getValue()*$multiplier);
113
    }
114
115 9
    public function add($addend)
116
    {
117 9
        if (!$addend instanceof self) {
118 7
            $addend = new self($addend);
119
        }
120 5
        $this->ensureSameType($addend, 'addend');
121
122 3
        if ($this->isAbsolute()) {
123 3
            $sum = $this->getValue()->add($addend->getValue());
0 ignored issues
show
Bug introduced by
It seems like $addend->getValue() can also be of type string; however, parameter $addends of Money\Money::add() does only seem to accept Money\Money, 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
            $sum = $this->getValue()->add(/** @scrutinizer ignore-type */ $addend->getValue());
Loading history...
124
        } else {
125 1
            $sum = $this->getValue() + $addend->getValue();
126
        }
127
128 3
        return new static($sum);
129
    }
130
131 1
    public function compare($other)
132
    {
133 1
        if (!$other instanceof self) {
134 1
            $other = new self($other);
135
        }
136 1
        $this->ensureSameType($other, 'comparison argument');
137
138 1
        if ($this->isAbsolute()) {
139 1
            return $this->value->compare($other->getValue());
0 ignored issues
show
Bug introduced by
It seems like $other->getValue() can also be of type string; however, parameter $other of Money\Money::compare() does only seem to accept Money\Money, 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

139
            return $this->value->compare(/** @scrutinizer ignore-type */ $other->getValue());
Loading history...
140
        } else {
141 1
            return $this->value - $other->getValue();
142
        }
143
    }
144
145 6
    public function ensureSameType(self $other, $name)
146
    {
147 6
        if ($this->isRelative() && !$other->isRelative()) {
148 1
            throw new FormulaSemanticsError("$name must be relative");
149
        }
150 5
        if ($this->isAbsolute() && !$other->isAbsolute()) {
151 1
            throw new FormulaSemanticsError("$name must be absolute");
152
        }
153 4
    }
154
155 4
    public function calculateSum(ChargeInterface $charge): Money
156
    {
157 4
        if ($this->value instanceof Money) {
158 2
            return $this->value->multiply($charge->getUsage()->getQuantity());
159
        }
160
161 2
        if ($this->value instanceof Quantity) {
0 ignored issues
show
introduced by
$this->value is never a sub-type of hiqdev\php\units\Quantity.
Loading history...
162
            $price = $charge->getPrice();
163
            if (!$price instanceof SinglePrice) {
164
                throw new \InvalidArgumentException('Discount based on quantity supports only single prices');
165
            }
166
167
            $compensatedQuantity = $this->calculateCompensatedQuantity($charge)->add($price->getPrepaid());
168
            return $price->calculateSum($compensatedQuantity);
169
        }
170
171
        return $charge->getSum()->multiply($this->value * 0.01);
172 2
    }
173
174
    private function calculateCompensatedQuantity(ChargeInterface $charge): QuantityInterface
175
    {
176
        $usage = $charge->getUsage();
177
        $maxCompensatedAmount = $this->getValue();
178
179
        switch ($usage->compare($maxCompensatedAmount)) {
0 ignored issues
show
Bug introduced by
$maxCompensatedAmount of type Money\Money|string is incompatible with the type hiqdev\php\units\QuantityInterface expected by parameter $other of hiqdev\php\units\QuantityInterface::compare(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

179
        switch ($usage->compare(/** @scrutinizer ignore-type */ $maxCompensatedAmount)) {
Loading history...
180
            case -1:
181
                return $usage;
182
            case 0:
183
            case 1:
184
                return $maxCompensatedAmount;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $maxCompensatedAmount returns the type Money\Money|string which is incompatible with the type-hinted return hiqdev\php\units\QuantityInterface.
Loading history...
Bug Best Practice introduced by
In this branch, the function will implicitly return null which is incompatible with the type-hinted return hiqdev\php\units\QuantityInterface. Consider adding a return statement or allowing null as return value.

For hinted functions/methods where all return statements with the correct type are only reachable via conditions, ?null? gets implicitly returned which may be incompatible with the hinted type. Let?s take a look at an example:

interface ReturnsInt {
    public function returnsIntHinted(): int;
}

class MyClass implements ReturnsInt {
    public function returnsIntHinted(): int
    {
        if (foo()) {
            return 123;
        }
        // here: null is implicitly returned
    }
}
Loading history...
185
        }
186
    }
187
}
188