MonthlyCap::modifyCharge()   A
last analyzed

Complexity

Conditions 4
Paths 4

Size

Total Lines 17
Code Lines 8

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 4
eloc 8
nc 4
nop 2
dl 0
loc 17
rs 10
c 1
b 0
f 0
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
        $chargeQuery = new ChargeDerivativeQuery();
155
        $chargeQuery->changeUsage(
156
            $this->getCapInHours()
157
                 ->multiply($coefficient)
158
                 ->multiply($action->getUsageInterval()->ratioOfMonth())
159
        );
160
        $chargeQuery->changeSum($charge->getSum()->multiply($coefficient));
161
162
        return $this->chargeDerivative->__invoke($charge, $chargeQuery);
163
    }
164
165
    private function quantityIsNotProportionalized(): bool
166
    {
167
        if (!$this->hasAddon('non-proportionalized')) {
168
            return false;
169
        }
170
171
        /** @var Boolean $addon */
172
        $addon = $this->getAddon('non-proportionalized');
173
174
        return $addon->value;
175
    }
176
}
177