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

FeatureContext::progressivePrice()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 17
Code Lines 14

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 14
c 0
b 0
f 0
dl 0
loc 17
rs 9.7998
cc 2
nc 2
nop 8

How to fix   Many Parameters   

Many Parameters

Methods with many parameters are not only hard to understand, but their parameters also often become inconsistent when you need more, or different data.

There are several approaches to avoid long parameter lists:

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\Behat\Context\Context;
14
use Behat\Behat\Tester\Exception\PendingException;
15
use Cache\Adapter\PHPArray\ArrayCachePool;
16
use Closure;
17
use DateTimeImmutable;
18
use Exception;
19
use hiqdev\php\billing\action\Action;
20
use hiqdev\php\billing\charge\Charge;
21
use hiqdev\php\billing\charge\ChargeInterface;
22
use hiqdev\php\billing\customer\Customer;
23
use hiqdev\php\billing\formula\FormulaEngine;
24
use hiqdev\php\billing\plan\Plan;
25
use hiqdev\php\billing\price\MoneyBuilder;
0 ignored issues
show
Bug introduced by
The type hiqdev\php\billing\price\MoneyBuilder 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...
26
use hiqdev\php\billing\price\PriceHelper;
0 ignored issues
show
Bug introduced by
The type hiqdev\php\billing\price\PriceHelper 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...
27
use hiqdev\php\billing\price\ProgressivePrice;
28
use hiqdev\php\billing\price\ProgressivePriceThreshold;
29
use hiqdev\php\billing\price\ProgressivePriceThresholdList;
30
use hiqdev\php\billing\sale\Sale;
31
use hiqdev\php\billing\price\SinglePrice;
32
use hiqdev\php\billing\target\Target;
33
use hiqdev\php\billing\tests\support\order\SimpleBilling;
34
use hiqdev\php\billing\type\Type;
35
use hiqdev\php\units\Quantity;
36
use hiqdev\php\units\Unit;
37
use Money\Currencies\ISOCurrencies;
38
use Money\Currency;
39
use Money\Money;
40
use Money\Parser\DecimalMoneyParser;
41
use NumberFormatter;
42
use PHPUnit\Framework\Assert;
43
use ReflectionClass;
44
45
/**
46
 * Defines application features from the specific context.
47
 */
48
class FeatureContext implements Context
49
{
50
    protected $engine;
51
52
    /** @var Customer */
53
    protected $customer;
54
    /**
55
     * @var \hiqdev\php\billing\price\PriceInterface|\hiqdev\php\billing\charge\FormulaChargeModifierTrait
0 ignored issues
show
Bug introduced by
The type hiqdev\php\billing\charg...mulaChargeModifierTrait 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...
56
     *
57
     * TODO: FormulaChargeModifierTrait::setFormula() must be moved to interface
58
     */
59
    protected $price;
60
61
    /** @var string */
62
    protected $formula;
63
64
    /**
65
     * @var \hiqdev\php\billing\action\ActionInterface|\hiqdev\php\billing\action\AbstractAction
66
     */
67
    protected $action;
68
    /**
69
     * @var ChargeInterface[]
70
     */
71
    protected $charges;
72
73
    /** @var \Money\MoneyParser */
74
    protected $moneyParser;
75
76
    /** @var string */
77
    protected $expectedError;
78
79
    /**
80
     * Initializes context.
81
     */
82
    public function __construct()
83
    {
84
        error_reporting(E_ALL & ~E_DEPRECATED);
85
        date_default_timezone_set('UTC');
86
        $this->customer = new Customer(null, 'somebody');
87
        $this->moneyParser = new DecimalMoneyParser(new ISOCurrencies());
88
        $this->plan = new Plan(null, 'plan', $this->customer);
0 ignored issues
show
Bug Best Practice introduced by
The property plan does not exist. Although not strictly required by PHP, it is generally a best practice to declare properties explicitly.
Loading history...
89
        $this->sale = new Sale(null, Target::any(), $this->customer, $this->plan, new DateTimeImmutable('2000-01-01'));
0 ignored issues
show
Bug Best Practice introduced by
The property sale does not exist. Although not strictly required by PHP, it is generally a best practice to declare properties explicitly.
Loading history...
90
        $this->billing = SimpleBilling::fromSale($this->sale);
0 ignored issues
show
Bug Best Practice introduced by
The property billing does not exist. Although not strictly required by PHP, it is generally a best practice to declare properties explicitly.
Loading history...
91
    }
92
93
    /**
94
     * @Given /(\S+) (\S+) price is ([0-9.]+) (\w+) per (\w+)(?: includes ([\d.]+))?/
95
     */
96
    public function priceIs($target, $type, $sum, $currency, $unit, $quantity = 0)
97
    {
98
        $type = new Type(Type::ANY, $type);
99
        $target = new Target(Target::ANY, $target);
100
        $quantity = Quantity::create($unit, $quantity);
101
        $sum = $this->moneyParser->parse($sum, new Currency($currency));
102
        $this->setPrice(new SinglePrice(null, $type, $target, null, $quantity, $sum));
103
    }
104
105
    protected array $progressivePrice = [];
106
    /**
107
     * @Given /(\S+) progressive price for (\S+) is +(\S+) (\S+) per (\S+) (\S+) (\S+) (\S+)$/
108
     */
109
    public function progressivePrice($target, $type, $price, $currency, $unit, $sign, $quantity, $perUnit): void
0 ignored issues
show
Unused Code introduced by
The parameter $perUnit 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

109
    public function progressivePrice($target, $type, $price, $currency, $unit, $sign, $quantity, /** @scrutinizer ignore-unused */ $perUnit): 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...
Unused Code introduced by
The parameter $sign 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

109
    public function progressivePrice($target, $type, $price, $currency, $unit, /** @scrutinizer ignore-unused */ $sign, $quantity, $perUnit): 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...
110
    {
111
        if (empty($this->progressivePrice[$type])) {
112
            $this->progressivePrice[$type] = [
113
                'target' => $target,
114
                'price' => $price,
115
                'currency' => $currency,
116
                'prepaid' => $quantity,
117
                'unit' => $unit,
118
                'thresholds' => [],
119
            ];
120
        } else {
121
            $this->progressivePrice[$type]['thresholds'][] = [
122
                'price' => $price,
123
                'currency' => $currency,
124
                'unit' => $unit,
125
                'quantity' => $quantity,
126
            ];
127
        }
128
    }
129
130
    /**
131
     * @Given /^build progressive price/
132
     */
133
    public function buildProgressivePrices()
134
    {
135
        $i = 0;
136
        foreach ($this->progressivePrice as $type => $price) {
137
            $type = new Type(Type::ANY, $type);
138
            $target = new Target(Target::ANY, $price['target']);
139
            $quantity = Quantity::create($price['unit'], $price['prepaid']);
140
            if ($i++ === 0) {
141
                $price['price'] *= 100;
142
            }
143
            $money = new Money($price['price'], new Currency($price['currency']));
144
            $thresholds = ProgressivePriceThresholdList::fromScalarsArray($price['thresholds']);
145
            $price = new ProgressivePrice(null, $type, $target, $quantity, $money, $thresholds);
146
            $this->setPrice($price);
147
        }
148
    }
149
150
    /**
151
     * @Given /sale close time is ([0-9.-]+)?/
152
     */
153
    public function setActionCloseTime($closeTime): void
154
    {
155
        if ($closeTime === null) {
156
            return;
157
        }
158
159
        $this->sale->close(new DateTimeImmutable($closeTime));
160
    }
161
162
    /**
163
     * @Given /sale time is (.+)$/
164
     */
165
    public function setSaleTime($time): void
166
    {
167
        $ref = new ReflectionClass($this->sale);
168
        $prop = $ref->getProperty('time');
169
        $prop->setAccessible(true);
170
        $prop->setValue($this->sale, new DateTimeImmutable($time));
171
        $prop->setAccessible(false);
172
    }
173
174
    private function setPrice($price)
175
    {
176
        $this->price = $price;
177
        $ref = new ReflectionClass($this->plan);
178
        $prop = $ref->getProperty('prices');
179
        $prop->setAccessible(true);
180
        $prop->setValue($this->plan, [$price]);
181
    }
182
183
    /**
184
     * @Given /action is (\S+) ([\w_,]+)(?: ([0-9.]+) (\S+))?(?: in (.+))?/
185
     */
186
    public function actionIs(string $target, string $type, float $amount, string $unit, ?string $date = null): void
187
    {
188
        $type = new Type(Type::ANY, $type);
189
        $target = new Target(Target::ANY, $target);
190
        $time = new DateTimeImmutable($date);
191
        if ($this->sale->getCloseTime() instanceof DateTimeImmutable) {
192
            $amount = $amount * $this->getFractionOfMonth(
193
                $time, $time, $this->sale->getCloseTime()
194
            );
195
        }
196
        $quantity = Quantity::create($unit, $amount);
197
198
        $this->action = new Action(null, $type, $target, $quantity, $this->customer, $time);
199
    }
200
201
    private function getFractionOfMonth(DateTimeImmutable $month, DateTimeImmutable $startTime, DateTimeImmutable $endTime): float
202
    {
203
        // SQL function: days2quantity()
204
205
        $month = $month->modify('first day of this month 00:00');
206
        if ($startTime < $month) {
207
            $startTime = $month;
208
        }
209
        if ($endTime > $month->modify('first day of next month 00:00')) {
210
            $endTime = $month->modify('first day of next month 00:00');
211
        }
212
213
        $secondsInMonth = $month->format('t') * 24 * 60 * 60;
214
215
        return ($endTime->getTimestamp() - $startTime->getTimestamp()) / $secondsInMonth;
216
    }
217
218
    /**
219
     * @Given /formula is (.+)/
220
     */
221
    public function formulaIs(string $formula): void
222
    {
223
        $this->formula = $formula;
224
    }
225
226
    /**
227
     * @Given /formula continues (.+)/
228
     */
229
    public function formulaContinues(string $formula): void
230
    {
231
        $this->formula .= "\n" . $formula;
232
    }
233
234
    protected function getFormulaEngine()
235
    {
236
        if ($this->engine === null) {
237
            $this->engine = new FormulaEngine(new ArrayCachePool());
238
        }
239
240
        return $this->engine;
241
    }
242
243
    /**
244
     * @When /action date is (.+)/
245
     * @throws Exception
246
     */
247
    public function actionDateIs(string $date): void
248
    {
249
        $this->action->setTime(new DateTimeImmutable($date));
0 ignored issues
show
Bug introduced by
The method setTime() does not exist on hiqdev\php\billing\action\ActionInterface. Since it exists in all sub-types, consider adding an abstract or default implementation to hiqdev\php\billing\action\ActionInterface. ( Ignorable by Annotation )

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

249
        $this->action->/** @scrutinizer ignore-call */ 
250
                       setTime(new DateTimeImmutable($date));
Loading history...
250
    }
251
252
    /**
253
     * @Given /^client rejected service at (.+)$/
254
     */
255
    public function actionCloseDateIs(string $close_date): void
256
    {
257
        $this->sale->close(new DateTimeImmutable($close_date));
258
    }
259
260
    /**
261
     * @Then /^error is$/m
262
     */
263
    public function multilineErrorIs(\Behat\Gherkin\Node\PyStringNode $value)
264
    {
265
        $this->expectedError = $value->getRaw();
266
    }
267
268
    /**
269
     * @Then /^error is (.+)$/
270
     *
271
     * @param string $error
272
     */
273
    public function errorIs($error): void
274
    {
275
        $this->expectedError = $error;
276
    }
277
278
    /**
279
     * @Then /^(\w+) charge is ?(?: with ?)?$/
280
     */
281
    public function emptyCharge(string $numeral): void
282
    {
283
        $this->chargeIs($numeral);
284
    }
285
286
    /**
287
     * @Then /^(\w+) charge is (\S+) +(-?[0-9.]+) ([A-Z]{3})(?: for ([\d.]+)? (\w+)?)?(?: with (.+)?)?$/
288
     */
289
    public function chargeWithSum($numeral, $type = null, $sum = null, $currency = null, $qty = null, $unit = null, $events = null): void
290
    {
291
        $this->chargeIs($numeral, $type, $sum, $currency, null, $qty, $unit, $events);
292
    }
293
294
    /**
295
     * @Then /^(\w+) charge is (\S+) +(-?[0-9.]+) ([A-Z]{3}) reason ([\w]+)(?: with (.+)?)?$/
296
     */
297
    public function chargeWithReason($numeral, $type = null, $sum = null, $currency = null, $reason = null, $events = null): void
298
    {
299
        $this->chargeIs($numeral, $type, $sum, $currency, $reason, null, null, $events);
300
    }
301
302
    public function chargeIs($numeral, $type = null, $sum = null, $currency = null, $reason = null, $qty = null, $unit = null, $events = null): void
303
    {
304
        $no = $this->ensureNo($numeral);
305
        if ($no === 0) {
306
            $this->calculatePrice();
307
        }
308
        $this->assertCharge($this->charges[$no] ?? null, $type, $sum, $currency, $reason, $qty, $unit, $events);
309
    }
310
311
    /**
312
     * @When /^calculating charges$/
313
     */
314
    public function calculatePrice(): void
315
    {
316
        $this->expectError(function () {
317
            if ($this->formula !== null) {
318
                $this->price->setModifier($this->getFormulaEngine()->build($this->formula));
0 ignored issues
show
Bug introduced by
The method setModifier() does not exist on hiqdev\php\billing\price\PriceInterface. Since it exists in all sub-types, consider adding an abstract or default implementation to hiqdev\php\billing\price\PriceInterface. ( Ignorable by Annotation )

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

318
                $this->price->/** @scrutinizer ignore-call */ 
319
                              setModifier($this->getFormulaEngine()->build($this->formula));
Loading history...
319
            }
320
            $this->charges = $this->billing->calculateCharges($this->action);
321
        });
322
    }
323
324
    public function expectError(Closure $closure): void
325
    {
326
        try {
327
            call_user_func($closure);
328
        } catch (Exception $e) {
329
            if ($this->isExpectedError($e)) {
330
                $this->expectedError = null;
331
            } else {
332
                throw $e;
333
            }
334
        }
335
        if ($this->expectedError) {
336
            throw new Exception('failed receive expected exception');
337
        }
338
    }
339
340
    protected function isExpectedError(Exception $e): bool
341
    {
342
        return str_starts_with($e->getMessage(), $this->expectedError);
343
    }
344
345
    /**
346
     * @param ChargeInterface|Charge|null $charge
347
     * @param string|null $type
348
     * @param string|null $sum
349
     * @param string|null $currency
350
     * @param string|null $reason
351
     * @param string|null $qty
352
     * @param string|null $unit
353
     * @param string|null $events
354
     */
355
    public function assertCharge($charge, $type, $sum, $currency, $reason, $qty, $unit, $events): void
356
    {
357
        if (empty($type) && empty($sum) && empty($currency)) {
358
            is_null($charge) || Assert::assertSame('0', $charge->getSum()->getAmount());
359
            return;
360
        }
361
        Assert::assertInstanceOf(Charge::class, $charge);
362
        Assert::assertSame($type, $this->normalizeType($charge->getType()->getName()), sprintf(
0 ignored issues
show
Bug introduced by
The method getType() does not exist on null. ( Ignorable by Annotation )

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

362
        Assert::assertSame($type, $this->normalizeType($charge->/** @scrutinizer ignore-call */ getType()->getName()), sprintf(

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...
363
            'Charge type %s does not match expected %s', $type, $this->normalizeType($charge->getType()->getName())
364
        ));
365
        $money = $this->moneyParser->parse($sum, new Currency($currency));
0 ignored issues
show
Bug introduced by
It seems like $currency can also be of type null; however, parameter $code of Money\Currency::__construct() 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

365
        $money = $this->moneyParser->parse($sum, new Currency(/** @scrutinizer ignore-type */ $currency));
Loading history...
Bug introduced by
It seems like $sum can also be of type null; however, parameter $money of Money\MoneyParser::parse() 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

365
        $money = $this->moneyParser->parse(/** @scrutinizer ignore-type */ $sum, new Currency($currency));
Loading history...
366
        Assert::assertTrue($money->equals($charge->getSum()), sprintf(
367
            'Charge sum %s does not match expected %s', $charge->getSum()->getAmount(), $money->getAmount()
368
        ));
369
        if ($reason !== null) {
370
            Assert::assertSame($reason, $charge->getComment(),
371
                sprintf('Charge comment %s does not match expected %s', $charge->getComment(), $reason)
372
            );
373
        }
374
        if ($qty !== null && $unit !== null) {
375
            Assert::assertEqualsWithDelta($qty, $charge->getUsage()->getQuantity(), 1e-7,
376
                sprintf('Charge quantity "%s" does not match expected "%s"', $charge->getUsage()->getQuantity(), $qty)
377
            );
378
            Assert::assertSame($unit, $charge->getUsage()->getUnit()->getName(),
379
                sprintf('Charge unit "%s" does not match expected "%s"', $charge->getUsage()->getUnit()->getName(), $unit)
380
            );
381
        }
382
        if ($events !== null) {
383
            $storedEvents = $charge->releaseEvents();
0 ignored issues
show
Bug introduced by
The method releaseEvents() does not exist on hiqdev\php\billing\charge\ChargeInterface. Since it exists in all sub-types, consider adding an abstract or default implementation to hiqdev\php\billing\charge\ChargeInterface. ( Ignorable by Annotation )

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

383
            /** @scrutinizer ignore-call */ 
384
            $storedEvents = $charge->releaseEvents();
Loading history...
384
            foreach (array_map('trim', explode(',', $events)) as $eventClass) {
385
                foreach ($storedEvents as $storedEvent) {
386
                    $eventReflection = new \ReflectionObject($storedEvent);
387
                    if ($eventReflection->getShortName() === $eventClass) {
388
                        continue 2;
389
                    }
390
                }
391
392
                Assert::fail(sprintf('Event of class %s is not present is charge', $eventClass));
393
            }
394
        } else {
395
            Assert::assertEmpty($charge->releaseEvents(), 'Failed asserting that charge does not have events');
396
        }
397
    }
398
399
    private function normalizeType($string): string
400
    {
401
        switch ($string) {
402
            case 'discount,discount':
403
                return 'discount';
404
            case 'monthly,leasing':
405
                return 'leasing';
406
            case 'monthly,installment':
407
                return 'installment';
408
            default:
409
                return $string;
410
        }
411
    }
412
413
    private function ensureNo(string $numeral): int
414
    {
415
        $formatter = new NumberFormatter('en_EN', NumberFormatter::SPELLOUT);
416
        $result = $formatter->parse($numeral);
417
        if ($result === false) {
418
            throw new Exception("Wrong numeral '$numeral'");
419
        }
420
421
        return --$result;
422
    }
423
424
    /**
425
     * @Given /^progressive price calculation steps are (.*)$/
426
     */
427
    public function progressivePriceCalculationStepsAre($explanation)
428
    {
429
        if (!$this->price instanceof ProgressivePrice) {
430
            throw new Exception('Price is not progressive');
431
        }
432
433
        $traces = array_map(fn($trace) => $trace->toShortString(), $this->price->getCalculationTraces());
434
        $billed = implode(' + ', $traces);
435
        Assert::assertSame($explanation, $billed, 'Progressive price calculation steps mismatch. Expected: ' . $explanation . ', got: ' . $billed);
436
    }
437
}
438