BillingContext   F
last analyzed

Complexity

Total Complexity 64

Size/Duplication

Total Lines 461
Duplicated Lines 0 %

Importance

Changes 22
Bugs 2 Features 0
Metric Value
eloc 150
dl 0
loc 461
rs 3.28
c 22
b 2
f 0
wmc 64

41 Methods

Rating   Name   Duplication   Size   Complexity  
A targetHasTheFollowingUses() 0 14 2
A fullPrice() 0 6 2
A getSaleQuantity() 0 3 1
A tariffPlanChangeIsRequestedForTargetAtSpecificTime() 0 3 1
A price() 0 3 1
A reseller() 0 3 1
A manager() 0 3 1
A tariffPlanChangeIsRequestedForTarget() 0 3 1
A days2quantity() 0 9 2
A plan() 0 6 2
A chargesNumber() 0 3 1
A billWithTime() 0 22 2
A prepareSum() 0 7 2
A recreatePlan() 0 3 1
A billsNumberWithTime() 0 9 1
A getNextCharge() 0 6 1
A enumPrice() 0 5 1
A saleClose() 0 3 1
A performBilling() 0 3 1
A priceWithTarget() 0 3 1
A setAction() 0 4 1
A prepareTime() 0 17 5
A sale() 0 4 1
A setConsumption() 0 4 1
A purchaseTarget() 0 4 1
A prepareQuantity() 0 7 2
A priceWithPrepaid() 0 3 1
B targetIsSoldToCustomerByPlanSinceTill() 0 26 10
A priceWithPrepaidAndTarget() 0 3 1
A customer() 0 3 1
A caughtErrorIs() 0 3 1
A target() 0 3 1
A targetHasExactlyNSaleForCustomer() 0 6 1
A performCalculation() 0 4 1
A findCharge() 0 14 4
A charge() 0 3 1
A chargeWithTarget() 0 12 1
A purchaseTargetWithInitialUses() 0 13 1
A targetHasExactlySalesForCustomer() 0 7 1
A flushEntitiesCache() 0 3 1
A findBill() 0 7 1

How to fix   Complexity   

Complex Class

Complex classes like BillingContext 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 BillingContext, 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-2020, HiQDev (http://hiqdev.com/)
9
 */
10
11
namespace hiqdev\php\billing\tests\behat\bootstrap;
12
13
use Behat\Gherkin\Node\TableNode;
14
use BehatExpectException\ExpectException;
15
use DateTimeImmutable;
16
use hiqdev\php\billing\bill\BillInterface;
17
use hiqdev\php\billing\charge\ChargeInterface;
18
use PHPUnit\Framework\Assert;
19
20
class BillingContext extends BaseContext
21
{
22
    use ExpectException {
23
        mayFail as protected;
24
        shouldFail as protected;
25
        assertCaughtExceptionMatches as protected;
26
    }
27
28
    protected $saleTime;
29
30
    protected $bill;
31
32
    protected $charges = [];
33
34
    /**
35
     * @Given reseller :reseller
36
     */
37
    public function reseller($reseller)
38
    {
39
        $this->builder->buildReseller($reseller);
40
    }
41
42
    /**
43
     * @Given customer :customer
44
     */
45
    public function customer($customer)
46
    {
47
        $this->builder->buildCustomer($customer);
48
    }
49
50
    /**
51
     * @Given manager :manager
52
     */
53
    public function manager($manager)
54
    {
55
        $this->builder->buildManager($manager);
0 ignored issues
show
Bug introduced by
The method buildManager() does not exist on hiqdev\php\billing\tests...tstrap\BuilderInterface. ( Ignorable by Annotation )

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

55
        $this->builder->/** @scrutinizer ignore-call */ 
56
                        buildManager($manager);

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
56
    }
57
58
    /**
59
     * @Given /^(\S+ )?(\S+) tariff plan (\S+)/
60
     */
61
    public function plan($prefix, $type, $plan)
62
    {
63
        $prefix = strtr($prefix, ' ', '_');
64
        $grouping = $prefix === 'grouping_';
65
        $type = $grouping ? $type : $prefix.$type;
66
        $this->builder->buildPlan($plan, $type, $grouping);
67
    }
68
69
    protected function fullPrice(array $data)
70
    {
71
        if (!empty($data['price'])) {
72
            $data['rate'] = $data['price'];
73
        }
74
        $this->builder->buildPrice($data);
75
    }
76
77
    /**
78
     * @Given /price for (\S+) is +(\S+) (\S+) per (\S+) for target (.+)$/
79
     */
80
    public function priceWithTarget($type, $price, $currency, $unit, $target)
81
    {
82
        return $this->fullPrice(compact('type', 'price', 'currency', 'unit', 'target'));
0 ignored issues
show
Bug introduced by
Are you sure the usage of $this->fullPrice(compact...cy', 'unit', 'target')) targeting hiqdev\php\billing\tests...ingContext::fullPrice() seems to always return null.

This check looks for function or method calls that always return null and whose return value is used.

class A
{
    function getObject()
    {
        return null;
    }

}

$a = new A();
if ($a->getObject()) {

The method getObject() can return nothing but null, so it makes no sense to use the return value.

The reason is most likely that a function or method is imcomplete or has been reduced for debug purposes.

Loading history...
83
    }
84
85
    /**
86
     * @Given /price for (\S+) is +(\S+) (\S+) per (\S+) prepaid (\S+)$/
87
     */
88
    public function priceWithPrepaid($type, $price, $currency, $unit, $prepaid)
89
    {
90
        return $this->fullPrice(compact('type', 'price', 'currency', 'unit', 'prepaid'));
0 ignored issues
show
Bug introduced by
Are you sure the usage of $this->fullPrice(compact...y', 'unit', 'prepaid')) targeting hiqdev\php\billing\tests...ingContext::fullPrice() seems to always return null.

This check looks for function or method calls that always return null and whose return value is used.

class A
{
    function getObject()
    {
        return null;
    }

}

$a = new A();
if ($a->getObject()) {

The method getObject() can return nothing but null, so it makes no sense to use the return value.

The reason is most likely that a function or method is imcomplete or has been reduced for debug purposes.

Loading history...
91
    }
92
93
    /**
94
     * @Given /price for (\S+) is +(\S+) (\S+) per (\S+) prepaid (\S+) for target (\S+)$/
95
     */
96
    public function priceWithPrepaidAndTarget($type, $price, $currency, $unit, $prepaid, $target)
97
    {
98
        return $this->fullPrice(compact('type', 'price', 'currency', 'unit', 'prepaid', 'target'));
0 ignored issues
show
Bug introduced by
Are you sure the usage of $this->fullPrice(compact..., 'prepaid', 'target')) targeting hiqdev\php\billing\tests...ingContext::fullPrice() seems to always return null.

This check looks for function or method calls that always return null and whose return value is used.

class A
{
    function getObject()
    {
        return null;
    }

}

$a = new A();
if ($a->getObject()) {

The method getObject() can return nothing but null, so it makes no sense to use the return value.

The reason is most likely that a function or method is imcomplete or has been reduced for debug purposes.

Loading history...
99
    }
100
101
    /**
102
     * @Given /price for (\S+) is +(\S+) (\S+) per (\S+)$/
103
     */
104
    public function price($type, $price, $currency, $unit)
105
    {
106
        return $this->fullPrice(compact('type', 'price', 'currency', 'unit'));
0 ignored issues
show
Bug introduced by
Are you sure the usage of $this->fullPrice(compact...', 'currency', 'unit')) targeting hiqdev\php\billing\tests...ingContext::fullPrice() seems to always return null.

This check looks for function or method calls that always return null and whose return value is used.

class A
{
    function getObject()
    {
        return null;
    }

}

$a = new A();
if ($a->getObject()) {

The method getObject() can return nothing but null, so it makes no sense to use the return value.

The reason is most likely that a function or method is imcomplete or has been reduced for debug purposes.

Loading history...
107
    }
108
109
    /**
110
     * @Given /price for (\S+) is +(\S+) (\S+) per 1 (\S+) and (\S+) (\S+) per 2 (\S+) for target (\S+)/
111
     */
112
    public function enumPrice($type, $price, $currency, $unit, $price2, $currency2, $unit2, $target)
0 ignored issues
show
Unused Code introduced by
The parameter $unit2 is not used and could be removed. ( Ignorable by Annotation )

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

112
    public function enumPrice($type, $price, $currency, $unit, $price2, $currency2, /** @scrutinizer ignore-unused */ $unit2, $target)

This check looks for parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
Unused Code introduced by
The parameter $currency2 is not used and could be removed. ( Ignorable by Annotation )

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

112
    public function enumPrice($type, $price, $currency, $unit, $price2, /** @scrutinizer ignore-unused */ $currency2, $unit2, $target)

This check looks for parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
113
    {
114
        $sums = [1 => $price, 2 => $price2];
115
116
        return $this->fullPrice(compact('type', 'sums', 'currency', 'unit', 'target'));
0 ignored issues
show
Bug introduced by
Are you sure the usage of $this->fullPrice(compact...cy', 'unit', 'target')) targeting hiqdev\php\billing\tests...ingContext::fullPrice() seems to always return null.

This check looks for function or method calls that always return null and whose return value is used.

class A
{
    function getObject()
    {
        return null;
    }

}

$a = new A();
if ($a->getObject()) {

The method getObject() can return nothing but null, so it makes no sense to use the return value.

The reason is most likely that a function or method is imcomplete or has been reduced for debug purposes.

Loading history...
117
    }
118
119
    /**
120
     * @Given /^remove and recreate tariff plan (\S+)/
121
     */
122
    public function recreatePlan($plan)
123
    {
124
        $this->builder->recreatePlan($plan);
125
    }
126
127
    /**
128
     * @Given /sale target (\S+) by plan (\S+) at (\S+)/
129
     */
130
    public function sale($target, $plan, $time): void
131
    {
132
        $this->saleTime = $this->prepareTime($time);
133
        $this->builder->buildSale($target, $plan, $this->saleTime);
0 ignored issues
show
Bug introduced by
It seems like $this->saleTime can also be of type null; however, parameter $time of hiqdev\php\billing\tests...rInterface::buildSale() does only seem to accept string, 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

133
        $this->builder->buildSale($target, $plan, /** @scrutinizer ignore-type */ $this->saleTime);
Loading history...
134
    }
135
    /**
136
     * @When /^sale close is requested for target "([^"]*)" at "([^"]*)", assuming current time is "([^"]*)"$/
137
     */
138
    public function saleClose(string $target, string $time, ?string $wallTime)
0 ignored issues
show
Unused Code introduced by
The parameter $time is not used and could be removed. ( Ignorable by Annotation )

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

138
    public function saleClose(string $target, /** @scrutinizer ignore-unused */ string $time, ?string $wallTime)

This check looks for parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
Unused Code introduced by
The parameter $target is not used and could be removed. ( Ignorable by Annotation )

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

138
    public function saleClose(/** @scrutinizer ignore-unused */ string $target, string $time, ?string $wallTime)

This check looks for parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
Unused Code introduced by
The parameter $wallTime is not used and could be removed. ( Ignorable by Annotation )

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

138
    public function saleClose(string $target, string $time, /** @scrutinizer ignore-unused */ ?string $wallTime)

This check looks for parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
139
    {
140
        throw new PendingException();
0 ignored issues
show
Bug introduced by
The type hiqdev\php\billing\tests...tstrap\PendingException was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
141
    }
142
143
    /**
144
     * @Then /^target "([^"]*)" has exactly (\d+) sale for customer$/
145
     */
146
    public function targetHasExactlyNSaleForCustomer(string $target, string $count)
0 ignored issues
show
Unused Code introduced by
The parameter $target is not used and could be removed. ( Ignorable by Annotation )

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

146
    public function targetHasExactlyNSaleForCustomer(/** @scrutinizer ignore-unused */ string $target, string $count)

This check looks for parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
147
    {
148
        // TODO: implement
149
        // $sales = $this->builder->findSales(['target-name' => $target]);
150
151
        Assert::assertCount($count, $sales);
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $sales seems to be never defined.
Loading history...
Bug introduced by
$count of type string is incompatible with the type integer expected by parameter $expectedCount of PHPUnit\Framework\Assert::assertCount(). ( Ignorable by Annotation )

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

151
        Assert::assertCount(/** @scrutinizer ignore-type */ $count, $sales);
Loading history...
152
    }
153
154
    /**
155
     * @Given /purchase target (\S+) by plan (\S+) at (\S+)$/
156
     */
157
    public function purchaseTarget(string $target, string $plan, string $time): void
158
    {
159
        $time = $this->prepareTime($time);
160
        $this->builder->buildPurchase($target, $plan, $time);
161
    }
162
163
    /**
164
     * @Given /^purchase target "([^"]*)" by plan "([^"]*)" at "([^"]*)" with the following initial uses:$/
165
     */
166
    public function purchaseTargetWithInitialUses(string $target, string $plan, string $time, TableNode $usesTable): void
167
    {
168
        $time = $this->prepareTime($time);
169
        $uses = array_map(static function (array $row) {
170
            return [
171
                'type' => $row['type'],
172
                'unit' => $row['unit'],
173
                'amount' => $row['amount'],
174
            ];
175
        }, $usesTable->getColumnsHash());
176
177
        $this->mayFail(
178
            fn() => $this->builder->buildPurchase($target, $plan, $time, $uses)
179
        );
180
    }
181
182
    /**
183
     * @Given /resource consumption for (\S+) is +(\S+) (\S+) for target (\S+) at (.+)$/
184
     */
185
    public function setConsumption(string $type, int $amount, string $unit, string $target, string $time): void
186
    {
187
        $time = $this->prepareTime($time);
188
        $this->builder->setConsumption($type, $amount, $unit, $target, $time);
0 ignored issues
show
Bug introduced by
The method setConsumption() does not exist on hiqdev\php\billing\tests...tstrap\BuilderInterface. Since it exists in all sub-types, consider adding an abstract or default implementation to hiqdev\php\billing\tests...tstrap\BuilderInterface. ( Ignorable by Annotation )

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

188
        $this->builder->/** @scrutinizer ignore-call */ 
189
                        setConsumption($type, $amount, $unit, $target, $time);
Loading history...
189
    }
190
191
    /**
192
     * @Given /perform billing at (\S+)/
193
     */
194
    public function performBilling(string $time): void
195
    {
196
        $this->builder->performBilling($this->prepareTime($time));
197
    }
198
199
    /**
200
     * @Given /action for (\S+) is +(\S+) (\S+) +for target (.+?)( +at (\S+))?$/
201
     */
202
    public function setAction(string $type, int $amount, string $unit, string $target, string $at = null, string $time = null): void
0 ignored issues
show
Unused Code introduced by
The parameter $at is not used and could be removed. ( Ignorable by Annotation )

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

202
    public function setAction(string $type, int $amount, string $unit, string $target, /** @scrutinizer ignore-unused */ string $at = null, string $time = null): void

This check looks for parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
203
    {
204
        $time = $this->prepareTime($time);
205
        $this->builder->setAction($type, $amount, $unit, $target, $time);
0 ignored issues
show
Bug introduced by
It seems like $time can also be of type null; however, parameter $time of hiqdev\php\billing\tests...rInterface::setAction() does only seem to accept string, 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

205
        $this->builder->setAction($type, $amount, $unit, $target, /** @scrutinizer ignore-type */ $time);
Loading history...
206
    }
207
208
    /**
209
     * @Given /perform calculation( at (\S+))?/
210
     */
211
    public function performCalculation(string $at = null, string $time = null): array
0 ignored issues
show
Unused Code introduced by
The parameter $at is not used and could be removed. ( Ignorable by Annotation )

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

211
    public function performCalculation(/** @scrutinizer ignore-unused */ string $at = null, string $time = null): array

This check looks for parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
212
    {
213
        $this->charges = $this->builder->performCalculation($this->prepareTime($time));
0 ignored issues
show
Bug introduced by
It seems like $this->prepareTime($time) can also be of type null; however, parameter $time of hiqdev\php\billing\tests...e::performCalculation() does only seem to accept string, 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

213
        $this->charges = $this->builder->performCalculation(/** @scrutinizer ignore-type */ $this->prepareTime($time));
Loading history...
214
        return $this->charges;
215
    }
216
217
    /**
218
     * @Given /bill +for (\S+) is +(\S+) (\S+) per (\S+) (\S+) for target (.+?)( +at (.+))?$/
219
     */
220
    public function billWithTime($type, $sum, $currency, $quantity, $unit, $target, $at = null, $time = null)
0 ignored issues
show
Unused Code introduced by
The parameter $at is not used and could be removed. ( Ignorable by Annotation )

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

220
    public function billWithTime($type, $sum, $currency, $quantity, $unit, $target, /** @scrutinizer ignore-unused */ $at = null, $time = null)

This check looks for parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
221
    {
222
        $this->builder->flushEntitiesCacheByType('bill');
223
224
        $quantity = $this->prepareQuantity($quantity);
225
        $sum = $this->prepareSum($sum, $quantity);
226
        $time = $this->prepareTime($time);
227
        $bill = $this->findBill([
228
            'type' => $type,
229
            'target' => $target,
230
            'sum' => "$sum $currency",
231
            'quantity' => "$quantity $unit",
232
            'time' => $time,
233
        ]);
234
        Assert::assertSame($type, $bill->getType()->getName());
235
        Assert::assertSame($target, $bill->getTarget()->getFullName());
236
        Assert::assertEquals(bcmul($sum, 100), $bill->getSum()->getAmount());
237
        Assert::assertSame($currency, $bill->getSum()->getCurrency()->getCode());
238
        Assert::assertEquals((float)$quantity, (float)$bill->getQuantity()->getQuantity());
239
        Assert::assertEquals(strtolower($unit), strtolower($bill->getQuantity()->getUnit()->getName()));
240
        if ($time) {
241
            Assert::assertEquals(new DateTimeImmutable($time), $bill->getTime());
242
        }
243
    }
244
245
    public function findBill(array $params): BillInterface
246
    {
247
        $bills = $this->builder->findBills($params);
248
        $this->bill = reset($bills);
249
        $this->charges = $this->bill->getCharges();
250
251
        return $this->bill;
252
    }
253
254
    /**
255
     * @Given /bills number is (\d+) for (\S+) for target (.+?)( +at (\S+))?$/
256
     */
257
    public function billsNumberWithTime($number, $type, $target, $at = null, $time = null)
0 ignored issues
show
Unused Code introduced by
The parameter $at is not used and could be removed. ( Ignorable by Annotation )

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

257
    public function billsNumberWithTime($number, $type, $target, /** @scrutinizer ignore-unused */ $at = null, $time = null)

This check looks for parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
258
    {
259
        $count = count($this->builder->findBills(array_filter([
260
            'type' => $type,
261
            'target' => $target,
262
            'time' => $this->prepareTime($time),
263
        ])));
264
265
        Assert::assertEquals($number, $count);
266
    }
267
268
    /**
269
     * @Given /charges number is (\d+)/
270
     */
271
    public function chargesNumber($number)
272
    {
273
        Assert::assertEquals($number, count($this->charges));
274
    }
275
276
    /**
277
     * @Given /charge for (\S+) is +(\S+) (\S+) per (\S+) (\S+)$/
278
     */
279
    public function charge($type, $amount, $currency, $quantity, $unit)
280
    {
281
        $this->chargeWithTarget($type, $amount, $currency, $quantity, $unit, null);
282
    }
283
284
    /**
285
     * @Given /charge for (\S+) is +(\S+) (\S+) per +(\S+) (\S+) +for target (.+?)( +at (\S+))?$/
286
     */
287
    public function chargeWithTarget($type, $amount, $currency, $quantity, $unit, $target, $at = null, $time = null)
0 ignored issues
show
Unused Code introduced by
The parameter $at is not used and could be removed. ( Ignorable by Annotation )

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

287
    public function chargeWithTarget($type, $amount, $currency, $quantity, $unit, $target, /** @scrutinizer ignore-unused */ $at = null, $time = null)

This check looks for parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
Unused Code introduced by
The parameter $time is not used and could be removed. ( Ignorable by Annotation )

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

287
    public function chargeWithTarget($type, $amount, $currency, $quantity, $unit, $target, $at = null, /** @scrutinizer ignore-unused */ $time = null)

This check looks for parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
288
    {
289
        $quantity = $this->prepareQuantity($quantity);
290
        $amount = $this->prepareSum($amount, $quantity);
291
        $charge = $this->findCharge($type, $target);
292
        Assert::assertNotNull($charge);
293
        Assert::assertSame($type, $charge->getType()->getName());
294
        Assert::assertSame($target, $charge->getTarget()->getFullName());
295
        Assert::assertEquals(bcmul($amount, 100), (int)$charge->getSum()->getAmount());
296
        Assert::assertSame($currency, $charge->getSum()->getCurrency()->getCode());
297
        Assert::assertEquals((float)$quantity, (float)$charge->getUsage()->getQuantity());
298
        Assert::assertEquals(strtolower($unit), strtolower($charge->getUsage()->getUnit()->getName()));
299
    }
300
301
    public function findCharge($type, $target): ?ChargeInterface
302
    {
303
        foreach ($this->charges as $charge) {
304
            if ($charge->getType()->getName() !== $type) {
305
                continue;
306
            }
307
            if ($charge->getTarget()->getFullName() !== $target) {
308
                continue;
309
            }
310
311
            return $charge;
312
        }
313
314
        return null;
315
    }
316
317
    public function getNextCharge(): ChargeInterface
318
    {
319
        $charge = current($this->charges);
320
        next($this->charges);
321
322
        return $charge;
323
    }
324
325
    /**
326
     * @return string|false|null
327
     */
328
    protected function prepareTime(string $time = null)
329
    {
330
        if ($time === null) {
331
            return null;
332
        }
333
334
        if ($time === 'midnight second day of this month') {
335
            return date('Y-m-02');
336
        }
337
        if (strncmp($time, 'pY', 1) === 0) {
338
            return date(substr($time, 1), strtotime('-1 year'));
339
        }
340
        if (strncmp($time, 'Y', 1) === 0) {
341
            return date($time);
342
        }
343
344
        return $time;
345
    }
346
347
    private function prepareQuantity($quantity)
348
    {
349
        if ($quantity[0] === 's') {
350
            return $this->getSaleQuantity();
351
        }
352
353
        return $quantity;
354
    }
355
356
    private function prepareSum($sum, $quantity)
357
    {
358
        if ($sum[0] === 's') {
359
            $sum = round(substr($sum, 1) * $quantity*100)/100;
360
        }
361
362
        return $sum;
363
    }
364
365
    public function getSaleQuantity()
366
    {
367
        return $this->days2quantity(new DateTimeImmutable($this->saleTime));
368
    }
369
370
    private function days2quantity(DateTimeImmutable $from)
371
    {
372
        $till = new DateTimeImmutable('first day of next month midnight');
373
        $diff = $from->diff($till);
374
        if ($diff->m) {
375
            return 1;
376
        }
377
378
        return $diff->d/date('t');
379
    }
380
381
    /**
382
     * @When /^tariff plan change is requested for target "([^"]*)" to plan "([^"]*)" at "([^"]*)"$/
383
     */
384
    public function tariffPlanChangeIsRequestedForTarget(string $target, string $planName, string $date)
385
    {
386
        $this->mayFail(fn () => $this->builder->targetChangePlan($target, $planName, $this->prepareTime($date)));
387
    }
388
389
    /**
390
     * @When /^tariff plan change is requested for target "([^"]*)" to plan "([^"]*)" at "([^"]*)", assuming current time is "([^"]*)"$/
391
     */
392
    public function tariffPlanChangeIsRequestedForTargetAtSpecificTime(string $target, string $planName, string $date, ?string $wallTime = null)
393
    {
394
        $this->mayFail(fn () => $this->builder->targetChangePlan($target, $planName, $this->prepareTime($date), $this->prepareTime($wallTime)));
395
    }
396
397
    /**
398
     * @Then /^target "([^"]*)" is sold to customer by plan "([^"]*)" since "([^"]*)"(?: till "([^"]*)")?$/
399
     */
400
    public function targetIsSoldToCustomerByPlanSinceTill(string $target, string $planName, string $saleDate, ?string $saleCloseDate = null)
401
    {
402
        $sales = $this->builder->findHistoricalSales([
403
            'target' => $target,
404
        ]);
405
406
        $saleDateTime = new DateTimeImmutable('@' . strtotime($saleDate));
407
        $saleCloseDateTime = new DateTimeImmutable('@' . ($saleCloseDate ? strtotime($saleCloseDate) : time()));
408
409
        foreach ($sales as $sale) {
410
            /** @noinspection PhpBooleanCanBeSimplifiedInspection */
411
            $saleExists = true
412
                && str_contains($sale->getPlan()->getName(), $planName)
413
                && $sale->getTime()->format(DATE_ATOM) === $saleDateTime->format(DATE_ATOM)
414
                && (
415
                    ($saleCloseDate === null && $sale->getCloseTime() === null)
416
                    ||
417
                    ($saleCloseDate !== null && $sale->getCloseTime()->format(DATE_ATOM) === $saleCloseDateTime->format(DATE_ATOM))
418
                );
419
420
            if ($saleExists) {
421
                return;
422
            }
423
        }
424
425
        Assert::fail('Requested sale does not exist');
426
    }
427
428
    /**
429
     * @Then /^target "([^"]*)" has exactly (\d+) sales for customer$/
430
     */
431
    public function targetHasExactlySalesForCustomer(string $target, int $count)
432
    {
433
        $sales = $this->builder->findHistoricalSales([
434
            'target' => $target,
435
        ]);
436
437
        Assert::assertCount($count, $sales);
438
    }
439
440
    /**
441
     * @Then /^caught error is "([^"]*)"$/
442
     */
443
    public function caughtErrorIs(string $errorMessage): void
444
    {
445
        $this->assertCaughtExceptionMatches(\Throwable::class, $errorMessage);
446
    }
447
448
    /**
449
     * @Given /^target "([^"]*)"$/
450
     */
451
    public function target(string $target)
452
    {
453
        $this->builder->buildTarget($target);
454
    }
455
456
    /**
457
     * @Then /^flush entities cache$/
458
     */
459
    public function flushEntitiesCache()
460
    {
461
        $this->builder->flushEntitiesCache();
462
    }
463
464
    /**
465
     * @Given /^target "([^"]*)" has the following uses:$/
466
     */
467
    public function targetHasTheFollowingUses(string $target, TableNode $usesTable)
468
    {
469
        foreach ($usesTable->getColumnsHash() as $row) {
470
            $uses = $this->builder->findUsage($row['time'], $target, $row['type']);
471
            Assert::assertCount(1, $uses);
472
473
            $use = reset($uses);
474
            Assert::assertSame(
475
                $row['unit'], $use['unit'],
476
                sprintf('Exptected unit to be %s, got %s instead', $row['unit'], $use['unit'])
477
            );
478
            Assert::assertEquals(
479
                $row['amount'], $use['total'],
480
                sprintf('Exptected total to be %s, got %s instead', $row['amount'], $use['total'])
481
            );
482
        }
483
    }
484
}
485