Once   A
last analyzed

Complexity

Total Complexity 23

Size/Duplication

Total Lines 119
Duplicated Lines 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 44
c 1
b 0
f 0
dl 0
loc 119
rs 10
wmc 23

13 Methods

Rating   Name   Duplication   Size   Complexity  
A createPeriod() 0 9 2
A modifyCharge() 0 14 2
A getIntervalMonthsFromPeriod() 0 9 3
A createNewZeroCharge() 0 10 2
A calculateMonthsDifference() 0 4 1
A getSaleTime() 0 8 3
A assertPeriod() 0 4 2
A isApplicable() 0 10 1
A per() 0 3 1
A getPer() 0 3 1
A isSupportedPeriod() 0 3 2
A __construct() 0 5 1
A assertCharge() 0 4 2
1
<?php declare(strict_types=1);
2
3
namespace hiqdev\php\billing\charge\modifiers;
4
5
use DateTimeImmutable;
6
use hiqdev\php\billing\action\ActionInterface;
7
use hiqdev\php\billing\charge\ChargeInterface;
8
use hiqdev\php\billing\charge\derivative\ChargeDerivative;
9
use hiqdev\php\billing\charge\derivative\ChargeDerivativeQuery;
10
use hiqdev\php\billing\charge\modifiers\addons\MonthPeriod;
11
use hiqdev\php\billing\charge\modifiers\addons\Period;
12
use hiqdev\php\billing\charge\modifiers\addons\YearPeriod;
13
use hiqdev\php\billing\formula\FormulaEngineException;
14
use Money\Money;
15
16
/**
17
 * 1. API:
18
 * - once.per('1 year') – bill every month that matches the month of sale of the object
19
 * - once.per('3 months') – bill every third month, starting from the month of sale of the object
20
 * - once.per('day'), once.per('1.5 months') – throws an interpret-time exception, a value must NOT be a fraction of the month
21
 * 2. In months where the formula should NOT bill, it should produce a ZERO charge.
22
 * 3. If the sale is re-opened, the formula starts over.
23
 */
24
class Once extends Modifier
25
{
26
    private const MONTHS_IN_YEAR_ON_EARTH = 12;
27
28
    protected ChargeDerivative $chargeDerivative;
29
30
    public function __construct(array $addons = [])
31
    {
32
        parent::__construct($addons);
33
34
        $this->chargeDerivative = new ChargeDerivative();
35
    }
36
37
    public function per(string $interval): self
38
    {
39
        return $this->addAddon('per', $this->createPeriod($interval));
40
    }
41
42
    private function createPeriod(string $interval): Period
43
    {
44
        $period = Period::fromString($interval);
45
46
        if ($this->isSupportedPeriod($period)) {
47
            return $period;
48
        }
49
50
        throw new FormulaEngineException("Invalid interval. Supported: whole months or years.");
51
    }
52
53
54
    private function isSupportedPeriod(Period $period): bool
55
    {
56
        return $period instanceof MonthPeriod || $period instanceof YearPeriod;
57
    }
58
59
    public function getPer(): ?Period
60
    {
61
        return $this->getAddon('per');
0 ignored issues
show
Bug Best Practice introduced by
The expression return $this->getAddon('per') could return the type hiqdev\php\billing\charge\modifiers\AddonInterface which includes types incompatible with the type-hinted return hiqdev\php\billing\charg...iers\addons\Period|null. Consider adding an additional type-check to rule them out.
Loading history...
62
    }
63
64
    public function modifyCharge(?ChargeInterface $charge, ActionInterface $action): array
65
    {
66
        $period = $this->getPer();
67
68
        $this->assertPeriod($period);
69
        $this->assertCharge($charge);
70
71
        // Apply the charge if applicable based on the action and interval period
72
        if ($this->isApplicable($action, $period)) {
73
            return [$charge];
74
        }
75
76
        // Return zero charge if the period is not applicable
77
        return [$this->createNewZeroCharge($charge)];
78
    }
79
80
    private function createNewZeroCharge(ChargeInterface $charge): ChargeInterface
81
    {
82
        $zeroChargeQuery = new ChargeDerivativeQuery();
83
        $zeroChargeQuery->changeSum(new Money(0, $charge->getSum()->getCurrency()));
84
        $reason = $this->getReason();
85
        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...
86
            $zeroChargeQuery->changeComment($reason->getValue());
87
        }
88
89
        return $this->chargeDerivative->__invoke($charge, $zeroChargeQuery);
90
    }
91
92
    private function assertPeriod(?Period $period)
93
    {
94
        if ($period === null) {
95
            throw new FormulaEngineException('Period cannot be null in Once');
96
        }
97
    }
98
99
    private function assertCharge(?ChargeInterface $charge)
100
    {
101
        if ($charge === null) {
102
            throw new FormulaEngineException('Charge cannot be null in Once');
103
        }
104
    }
105
106
    private function isApplicable(ActionInterface $action, Period $period): bool
107
    {
108
        $saleTime = $this->getSaleTime($action);
109
        $actionTime = $action->getTime();
110
        $monthsDiff = $this->calculateMonthsDifference($saleTime, $actionTime);
111
112
        $intervalMonths = $this->getIntervalMonthsFromPeriod($period);
113
114
        // Check if the current period is applicable (divisible by interval)
115
        return $monthsDiff % $intervalMonths === 0;
116
    }
117
118
    private function getSaleTime(ActionInterface $action): DateTimeImmutable
119
    {
120
        $sale = $action->getSale();
121
        if ($sale === null || $sale->getTime() === null) {
122
            throw new FormulaEngineException('Sale or sale time cannot be null in Once');
123
        }
124
125
        return $sale->getTime();
126
    }
127
128
    private function calculateMonthsDifference(DateTimeImmutable $start, DateTimeImmutable $end): int
129
    {
130
        $interval = $end->diff($start);
131
        return ($interval->y * self::MONTHS_IN_YEAR_ON_EARTH) + $interval->m;
132
    }
133
134
    private function getIntervalMonthsFromPeriod(Period $period): int
135
    {
136
        if ($period instanceof YearPeriod) {
137
            return self::MONTHS_IN_YEAR_ON_EARTH;
138
        } elseif ($period instanceof MonthPeriod) {
139
            return $period->getValue();
140
        }
141
142
        throw new FormulaEngineException('Unsupported interval period in Once');
143
    }
144
}
145