Passed
Push — master ( 54e3cc...a9003e )
by Dmitry
14:11
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
44
/**
45
 * Defines application features from the specific context.
46
 */
47
class FeatureContext implements Context
48
{
49
    protected $engine;
50
51
    /** @var Customer */
52
    protected $customer;
53
    /**
54
     * @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...
55
     *
56
     * TODO: FormulaChargeModifierTrait::setFormula() must be moved to interface
57
     */
58
    protected $price;
59
60
    /** @var string */
61
    protected $formula;
62
63
    /**
64
     * @var \hiqdev\php\billing\action\ActionInterface|\hiqdev\php\billing\action\AbstractAction
65
     */
66
    protected $action;
67
    /**
68
     * @var ChargeInterface[]
69
     */
70
    protected $charges;
71
72
    /** @var \Money\MoneyParser */
73
    protected $moneyParser;
74
75
    /** @var string */
76
    protected $expectedError;
77
78
    /**
79
     * Initializes context.
80
     */
81
    public function __construct()
82
    {
83
        error_reporting(E_ALL & ~E_DEPRECATED);
84
        date_default_timezone_set('UTC');
85
        $this->customer = new Customer(null, 'somebody');
86
        $this->moneyParser = new DecimalMoneyParser(new ISOCurrencies());
87
        $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...
88
        $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...
89
        $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...
90
    }
91
92
    /**
93
     * @Given /(\S+) (\S+) price is ([0-9.]+) (\w+) per (\w+)(?: includes ([\d.]+))?/
94
     */
95
    public function priceIs($target, $type, $sum, $currency, $unit, $quantity = 0)
96
    {
97
        $type = new Type(Type::ANY, $type);
98
        $target = new Target(Target::ANY, $target);
99
        $quantity = Quantity::create($unit, $quantity);
100
        $sum = $this->moneyParser->parse($sum, new Currency($currency));
101
        $this->setPrice(new SinglePrice(null, $type, $target, null, $quantity, $sum));
102
    }
103
104
    protected array $progressivePrice = [];
105
    /**
106
     * @Given /(\S+) progressive price for (\S+) is +(\S+) (\S+) per (\S+) (\S+) (\S+) (\S+)$/
107
     */
108
    public function progressivePrice($target, $type, $price, $currency, $unit, $sign, $quantity, $perUnit): void
0 ignored issues
show
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

108
    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...
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

108
    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...
109
    {
110
        if (empty($this->progressivePrice[$type])) {
111
            $this->progressivePrice[$type] = [
112
                'target' => $target,
113
                'price' => $price,
114
                'currency' => $currency,
115
                'prepaid' => $quantity,
116
                'unit' => $unit,
117
                'thresholds' => [],
118
            ];
119
        } else {
120
            $this->progressivePrice[$type]['thresholds'][] = [
121
                'price' => $price,
122
                'currency' => $currency,
123
                'unit' => $unit,
124
                'quantity' => $quantity,
125
            ];
126
        }
127
    }
128
129
    /**
130
     * @Given /^build progressive price/
131
     */
132
    public function buildProgressivePrices()
133
    {
134
        foreach ($this->progressivePrice as $type => $price) {
135
            $type = new Type(Type::ANY, $type);
136
            $target = new Target(Target::ANY, $price['target']);
137
            $quantity = Quantity::create($price['unit'], $price['prepaid']);
138
            $money = new Money($price['price'], new Currency($price['currency']));
139
            $thresholds = ProgressivePriceThresholdList::fromScalarsArray($price['thresholds']);
140
            $price = new ProgressivePrice(null, $type, $target, $quantity, $money, $thresholds);
141
            $this->setPrice($price);
142
        }
143
    }
144
145
    /**
146
     * @Given /sale close time is ([0-9.-]+)/
147
     */
148
    public function setActionCloseTime($closeTime): void
149
    {
150
        $this->sale->close(new DateTimeImmutable($closeTime));
151
    }
152
153
    private function setPrice($price)
154
    {
155
        $this->price = $price;
156
        $ref = new \ReflectionClass($this->plan);
157
        $prop = $ref->getProperty('prices');
158
        $prop->setAccessible(true);
159
        $prop->setValue($this->plan, [$price]);
160
    }
161
162
    /**
163
     * @Given /action is (\S+) ([\w_,]+)(?: ([0-9.]+) (\S+))?(?: in (.+))?/
164
     */
165
    public function actionIs(string $target, string $type, float $amount, string $unit, ?string $date = null): void
166
    {
167
        $type = new Type(Type::ANY, $type);
168
        $target = new Target(Target::ANY, $target);
169
        $time = new DateTimeImmutable($date);
170
        if ($this->sale->getCloseTime() instanceof DateTimeImmutable) {
171
            $amount = $amount * $this->getFractionOfMonth(
172
                $time, $time, $this->sale->getCloseTime()
173
            );
174
        }
175
        $quantity = Quantity::create($unit, $amount);
176
177
        $this->action = new Action(null, $type, $target, $quantity, $this->customer, $time);
178
    }
179
180
    private function getFractionOfMonth(DateTimeImmutable $month, DateTimeImmutable $startTime, DateTimeImmutable $endTime): float
181
    {
182
        // SQL function: days2quantity()
183
184
        $month = $month->modify('first day of this month 00:00');
185
        if ($startTime < $month) {
186
            $startTime = $month;
187
        }
188
        if ($endTime > $month->modify('first day of next month 00:00')) {
189
            $endTime = $month->modify('first day of next month 00:00');
190
        }
191
192
        $secondsInMonth = $month->format('t') * 24 * 60 * 60;
193
194
        return ($endTime->getTimestamp() - $startTime->getTimestamp()) / $secondsInMonth;
195
    }
196
197
    /**
198
     * @Given /formula is (.+)/
199
     */
200
    public function formulaIs(string $formula): void
201
    {
202
        $this->formula = $formula;
203
    }
204
205
    /**
206
     * @Given /formula continues (.+)/
207
     */
208
    public function formulaContinues(string $formula): void
209
    {
210
        $this->formula .= "\n" . $formula;
211
    }
212
213
    protected function getFormulaEngine()
214
    {
215
        if ($this->engine === null) {
216
            $this->engine = new FormulaEngine(new ArrayCachePool());
217
        }
218
219
        return $this->engine;
220
    }
221
222
    /**
223
     * @When /action date is (.+)/
224
     * @throws Exception
225
     */
226
    public function actionDateIs(string $date): void
227
    {
228
        $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

228
        $this->action->/** @scrutinizer ignore-call */ 
229
                       setTime(new DateTimeImmutable($date));
Loading history...
229
    }
230
231
    /**
232
     * @Given /^client rejected service at (.+)$/
233
     */
234
    public function actionCloseDateIs(string $close_date): void
235
    {
236
        $this->sale->close(new DateTimeImmutable($close_date));
237
    }
238
239
    /**
240
     * @Then /^error is$/m
241
     */
242
    public function multilineErrorIs(\Behat\Gherkin\Node\PyStringNode $value)
243
    {
244
        $this->expectedError = $value->getRaw();
245
    }
246
247
    /**
248
     * @Then /^error is (.+)$/
249
     *
250
     * @param string $error
251
     */
252
    public function errorIs($error): void
253
    {
254
        $this->expectedError = $error;
255
    }
256
257
    /**
258
     * @Then /^(\w+) charge is ?(?: with ?)?$/
259
     */
260
    public function emptyCharge(string $numeral): void
261
    {
262
        $this->chargeIs($numeral);
263
    }
264
265
    /**
266
     * @Then /^(\w+) charge is (\S+) +(-?[0-9.]+) ([A-Z]{3})(?: for ([\d.]+)? (\w+)?)?(?: with (.+)?)?$/
267
     */
268
    public function chargeWithSum($numeral, $type = null, $sum = null, $currency = null, $qty = null, $unit = null, $events = null): void
269
    {
270
        $this->chargeIs($numeral, $type, $sum, $currency, null, $qty, $unit, $events);
271
    }
272
273
    /**
274
     * @Then /^(\w+) charge is (\S+) +(-?[0-9.]+) ([A-Z]{3}) reason ([\w]+)(?: with (.+)?)?$/
275
     */
276
    public function chargeWithReason($numeral, $type = null, $sum = null, $currency = null, $reason = null, $events = null): void
277
    {
278
        $this->chargeIs($numeral, $type, $sum, $currency, $reason, null, null, $events);
279
    }
280
281
    public function chargeIs($numeral, $type = null, $sum = null, $currency = null, $reason = null, $qty = null, $unit = null, $events = null): void
282
    {
283
        $no = $this->ensureNo($numeral);
284
        if ($no === 0) {
285
            $this->calculatePrice();
286
        }
287
        $this->assertCharge($this->charges[$no] ?? null, $type, $sum, $currency, $reason, $qty, $unit, $events);
288
    }
289
290
    /**
291
     * @When /^calculating charges$/
292
     */
293
    public function calculatePrice(): void
294
    {
295
        $this->expectError(function () {
296
            if ($this->formula !== null) {
297
                $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

297
                $this->price->/** @scrutinizer ignore-call */ 
298
                              setModifier($this->getFormulaEngine()->build($this->formula));
Loading history...
298
            }
299
            $this->charges = $this->billing->calculateCharges($this->action);
300
        });
301
    }
302
303
    public function expectError(Closure $closure): void
304
    {
305
        try {
306
            call_user_func($closure);
307
        } catch (Exception $e) {
308
            if ($this->isExpectedError($e)) {
309
                $this->expectedError = null;
310
            } else {
311
                throw $e;
312
            }
313
        }
314
        if ($this->expectedError) {
315
            throw new Exception('failed receive expected exception');
316
        }
317
    }
318
319
    protected function isExpectedError(Exception $e): bool
320
    {
321
        return str_starts_with($e->getMessage(), $this->expectedError);
322
    }
323
324
    /**
325
     * @param ChargeInterface|Charge|null $charge
326
     * @param string|null $type
327
     * @param string|null $sum
328
     * @param string|null $currency
329
     * @param string|null $reason
330
     * @param string|null $qty
331
     * @param string|null $unit
332
     * @param string|null $events
333
     */
334
    public function assertCharge($charge, $type, $sum, $currency, $reason, $qty, $unit, $events): void
335
    {
336
        if (empty($type) && empty($sum) && empty($currency)) {
337
            is_null($charge) || Assert::assertSame('0', $charge->getSum()->getAmount());
338
            return;
339
        }
340
        Assert::assertInstanceOf(Charge::class, $charge);
341
        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

341
        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...
342
            'Charge type %s does not match expected %s', $type, $this->normalizeType($charge->getType()->getName())
343
        ));
344
        $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

344
        $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

344
        $money = $this->moneyParser->parse(/** @scrutinizer ignore-type */ $sum, new Currency($currency));
Loading history...
345
        Assert::assertTrue($money->equals($charge->getSum()), sprintf(
346
            'Charge sum %s does not match expected %s', $charge->getSum()->getAmount(), $money->getAmount()
347
        ));
348
        if ($reason !== null) {
349
            Assert::assertSame($reason, $charge->getComment(),
350
                sprintf('Charge comment %s does not match expected %s', $charge->getComment(), $reason)
351
            );
352
        }
353
        if ($qty !== null && $unit !== null) {
354
            Assert::assertEqualsWithDelta($qty, $charge->getUsage()->getQuantity(), 1e-7,
355
                sprintf('Charge quantity "%s" does not match expected "%s"', $charge->getUsage()->getQuantity(), $qty)
356
            );
357
            Assert::assertSame($unit, $charge->getUsage()->getUnit()->getName(),
358
                sprintf('Charge unit "%s" does not match expected "%s"', $charge->getUsage()->getUnit()->getName(), $unit)
359
            );
360
        }
361
        if ($events !== null) {
362
            $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

362
            /** @scrutinizer ignore-call */ 
363
            $storedEvents = $charge->releaseEvents();
Loading history...
363
            foreach (array_map('trim', explode(',', $events)) as $eventClass) {
364
                foreach ($storedEvents as $storedEvent) {
365
                    $eventReflection = new \ReflectionObject($storedEvent);
366
                    if ($eventReflection->getShortName() === $eventClass) {
367
                        continue 2;
368
                    }
369
                }
370
371
                Assert::fail(sprintf('Event of class %s is not present is charge', $eventClass));
372
            }
373
        } else {
374
            Assert::assertEmpty($charge->releaseEvents(), 'Failed asserting that charge does not have events');
375
        }
376
    }
377
378
    private function normalizeType($string): string
379
    {
380
        switch ($string) {
381
            case 'discount,discount':
382
                return 'discount';
383
            case 'monthly,leasing':
384
                return 'leasing';
385
            case 'monthly,installment':
386
                return 'installment';
387
            default:
388
                return $string;
389
        }
390
    }
391
392
    private function ensureNo(string $numeral): int
393
    {
394
        $formatter = new NumberFormatter('en_EN', NumberFormatter::SPELLOUT);
395
        $result = $formatter->parse($numeral);
396
        if ($result === false) {
397
            throw new Exception("Wrong numeral '$numeral'");
398
        }
399
400
        return --$result;
401
    }
402
403
    /**
404
     * @Given /^progressive price calculation steps are (.*)$/
405
     */
406
    public function progressivePriceCalculationStepsAre($explanation)
407
    {
408
        if (!$this->price instanceof ProgressivePrice) {
409
            throw new Exception('Price is not progressive');
410
        }
411
412
        $traces = array_map(fn($trace) => $trace->toShortString(), $this->price->getCalculationTraces());
413
        $billed = implode(' + ', $traces);
414
        Assert::assertSame($explanation, $billed, 'Progressive price calculation steps mismatch. Expected: ' . $explanation . ', got: ' . $billed);
415
    }
416
}
417