Passed
Push — master ( 75ba92...aea3e4 )
by Dmitry
14:02
created

MonthlyCap::propotionalizeCharge()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 15
Code Lines 8

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 8
c 0
b 0
f 0
dl 0
loc 15
rs 10
cc 2
nc 2
nop 2
1
<?php
2
3
declare(strict_types=1);
4
5
/**
6
 * PHP Billing Library
7
 *
8
 * @link      https://github.com/hiqdev/php-billing
9
 * @package   php-billing
10
 * @license   BSD-3-Clause
11
 * @copyright Copyright (c) 2017-2020, HiQDev (http://hiqdev.com/)
12
 */
13
14
namespace hiqdev\php\billing\charge\modifiers;
15
16
use hiqdev\php\billing\action\ActionInterface;
17
use hiqdev\php\billing\charge\ChargeInterface;
18
use hiqdev\php\billing\charge\derivative\ChargeDerivative;
19
use hiqdev\php\billing\charge\derivative\ChargeDerivativeQuery;
20
use hiqdev\php\billing\charge\modifiers\addons\Boolean;
21
use hiqdev\php\billing\charge\modifiers\addons\DayPeriod;
22
use hiqdev\php\billing\charge\modifiers\addons\Period;
23
use hiqdev\php\billing\Exception\NotSupportedException;
24
use hiqdev\php\units\Quantity;
25
use hiqdev\php\units\QuantityInterface;
26
use Money\Money;
27
28
/**
29
 * Monthly cap represents a maximum number of days in a month,
30
 * that can be charged.
31
 *
32
 * @author Andrii Vasyliev <[email protected]>
33
 * @author Dmytro Naumenko <[email protected]>
34
 */
35
class MonthlyCap extends Modifier
36
{
37
    private const PERIOD = 'period';
38
39
    protected ChargeDerivative $chargeDerivative;
40
41
    public function __construct(string $capDuration, array $addons = [])
42
    {
43
        parent::__construct($addons);
44
45
        $period = Period::fromString($capDuration);
46
        if (!$period instanceof DayPeriod) {
47
            throw new NotSupportedException('Only number of days in month is supported');
48
        }
49
50
        $this->addAddon(self::PERIOD, $period);
51
        $this->chargeDerivative = new ChargeDerivative();
52
    }
53
54
    public function forNonProportionalizedQuantity(): Modifier
55
    {
56
        return $this->addAddon('non-proportionalized', new Boolean(true));
57
    }
58
59
    public function getNext()
60
    {
61
        return $this;
62
    }
63
64
    public function modifyCharge(?ChargeInterface $charge, ActionInterface $action): array
65
    {
66
        if ($charge === null) {
67
            return [];
68
        }
69
70
        $month = $action->getTime()->modify('first day of this month midnight');
71
        /** @noinspection PhpUnhandledExceptionInspection */
72
        if (!$this->checkPeriod($month)) {
73
            return [$charge];
74
        }
75
76
        if ($this->quantityIsNotProportionalized()) {
77
            return $this->propotionalizeCharge($charge, $action);
78
        }
79
80
        return $this->splitCapFromCharge($charge, $action);
81
    }
82
83
    /**
84
     * @param ChargeInterface $charge
85
     * @param ActionInterface $action
86
     * @return ChargeInterface[]
87
     */
88
    private function propotionalizeCharge(ChargeInterface $charge, ActionInterface $action): array
89
    {
90
        $usedHours = $action->getUsageInterval()->hours();
91
        $capInHours = $this->getCapInHours()->getQuantity();
92
93
        if ($usedHours > $capInHours) {
94
            return [$charge];
95
        }
96
97
        $chargeQuery = new ChargeDerivativeQuery();
98
        $chargeQuery->changeSum(
99
            $charge->getSum()->multiply(sprintf('%.14F', ($usedHours / $capInHours)))
100
        );
101
102
        return [$this->chargeDerivative->__invoke($charge, $chargeQuery)];
103
    }
104
105
    private function getCapInHours(): QuantityInterface
106
    {
107
        $cap = $this->getAddon(self::PERIOD);
108
        assert($cap instanceof DayPeriod, 'Cap can be only a DayPeriod');
109
110
        return Quantity::create('hour', $cap->getValue() * 24);
111
    }
112
113
    private function splitCapFromCharge(ChargeInterface $charge, ActionInterface $action): array
114
    {
115
        $charge = $this->makeMappedCharge($charge, $action);
116
117
        $usageHours = Quantity::create('hour', $action->getUsageInterval()->hours());
118
        $cappedHours = $this->getCapInHours();
119
        $usageExceedsCap = $usageHours->compare($cappedHours) === 1;
120
        if (!$usageExceedsCap) {
121
            return [$charge];
122
        }
123
124
        $diffRatio = 1 - ($usageHours->subtract($cappedHours)->getQuantity() / $usageHours->getQuantity());
125
126
        $chargeQuery = new ChargeDerivativeQuery();
127
        $chargeQuery->changeUsage($cappedHours);
128
        $chargeQuery->changeSum($charge->getSum()->multiply(sprintf('%.14F', $diffRatio), Money::ROUND_HALF_DOWN));
129
        $newCharge = $this->chargeDerivative->__invoke($charge, $chargeQuery);
130
131
        $zeroChargeQuery = new ChargeDerivativeQuery();
132
        $zeroChargeQuery->changeSum(new Money(0, $charge->getSum()->getCurrency()));
133
        $zeroChargeQuery->changeUsage($usageHours->subtract($cappedHours));
134
        $zeroChargeQuery->changeParent($newCharge);
135
        $reason = $this->getReason();
136
        if ($reason) {
0 ignored issues
show
introduced by
$reason is of type hiqdev\php\billing\charge\modifiers\addons\Reason, thus it always evaluated to true.
Loading history...
137
            $zeroChargeQuery->changeComment($reason->getValue());
138
        }
139
        $newZeroCharge = $this->chargeDerivative->__invoke($charge, $zeroChargeQuery);
140
141
        return [$newCharge, $newZeroCharge];
142
    }
143
144
    private function getEffectiveCoefficient(ActionInterface $action): string
145
    {
146
        $hoursInMonth = $action->getTime()->format('t') * 24;
147
148
        return sprintf('%.14F', 1 / ($this->getCapInHours()->getQuantity() / $hoursInMonth));
149
    }
150
151
    private function makeMappedCharge(ChargeInterface $charge, ActionInterface $action): ChargeInterface
152
    {
153
        $coefficient = $this->getEffectiveCoefficient($action);
154
        $quantityUnderCap = $charge->getUsage()->multiply((string)$coefficient);
155
        $chargeQuery = new ChargeDerivativeQuery();
156
        $chargeQuery->changeUsage(
157
            $this->getCapInHours()->multiply((string)$quantityUnderCap->getQuantity())
158
        );
159
        $chargeQuery->changeSum($charge->getSum()->multiply((string)$coefficient));
160
161
        return $this->chargeDerivative->__invoke($charge, $chargeQuery);
162
    }
163
164
    private function quantityIsNotProportionalized(): bool
165
    {
166
        if (!$this->hasAddon('non-proportionalized')) {
167
            return false;
168
        }
169
170
        /** @var Boolean $addon */
171
        $addon = $this->getAddon('non-proportionalized');
172
173
        return $addon->value;
174
    }
175
}
176