Passed
Push — master ( e39bb0...54e3cc )
by Dmitry
14:48
created

FeatureContext::progressivePrice()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 25
Code Lines 18

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 2
eloc 18
c 0
b 0
f 0
nc 2
nop 8
dl 0
loc 25
rs 9.6666

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;
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\sale\Sale;
30
use hiqdev\php\billing\price\SinglePrice;
31
use hiqdev\php\billing\target\Target;
32
use hiqdev\php\billing\tests\support\order\SimpleBilling;
33
use hiqdev\php\billing\type\Type;
34
use hiqdev\php\units\Quantity;
35
use hiqdev\php\units\Unit;
36
use Money\Currencies\ISOCurrencies;
37
use Money\Currency;
38
use Money\Money;
39
use Money\Parser\DecimalMoneyParser;
40
use NumberFormatter;
41
use PHPUnit\Framework\Assert;
42
43
/**
44
 * Defines application features from the specific context.
45
 */
46
class FeatureContext implements Context
47
{
48
    protected $engine;
49
50
    /** @var Customer */
51
    protected $customer;
52
    /**
53
     * @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...
54
     *
55
     * TODO: FormulaChargeModifierTrait::setFormula() must be moved to interface
56
     */
57
    protected $price;
58
59
    /** @var string */
60
    protected $formula;
61
62
    /**
63
     * @var \hiqdev\php\billing\action\ActionInterface|\hiqdev\php\billing\action\AbstractAction
64
     */
65
    protected $action;
66
    /**
67
     * @var ChargeInterface[]
68
     */
69
    protected $charges;
70
71
    /** @var \Money\MoneyParser */
72
    protected $moneyParser;
73
74
    /** @var string */
75
    protected $expectedError;
76
77
    /**
78
     * Initializes context.
79
     */
80
    public function __construct()
81
    {
82
        error_reporting(E_ALL & ~E_DEPRECATED);
83
        date_default_timezone_set('UTC');
84
        $this->customer = new Customer(null, 'somebody');
85
        $this->moneyParser = new DecimalMoneyParser(new ISOCurrencies());
86
        $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...
87
        $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...
88
        $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...
89
    }
90
91
    /**
92
     * @Given /(\S+) (\S+) price is ([0-9.]+) (\w+) per (\w+)(?: includes ([\d.]+))?/
93
     */
94
    public function priceIs($target, $type, $sum, $currency, $unit, $quantity = 0)
95
    {
96
        $type = new Type(Type::ANY, $type);
97
        $target = new Target(Target::ANY, $target);
98
        $quantity = Quantity::create($unit, $quantity);
99
        $sum = $this->moneyParser->parse($sum, new Currency($currency));
100
        $this->setPrice(new SinglePrice(null, $type, $target, null, $quantity, $sum));
101
    }
102
103
    /**
104
     * @Given /(\S+) progressive price for (\S+) is +(\S+) (\S+) per (\S+) (\S+) (\S+) (\S+)$/
105
     */
106
    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

106
    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

106
    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...
107
    {
108
        if (empty($this->progressivePrice[$type])) {
109
            $this->progressivePrice[$type] = [
0 ignored issues
show
Bug Best Practice introduced by
The property progressivePrice does not exist. Although not strictly required by PHP, it is generally a best practice to declare properties explicitly.
Loading history...
110
                'target' => $target,
111
                'unit' => $unit,
112
                'price' => $price,
113
                'currency' => $currency,
114
                'thresholds' =>[
115
                    [
116
                        'price' => $price,
117
                        'currency' => $currency,
118
                        'unit' => $unit,
119
                        'quantity' => $quantity,
120
                    ]
121
                ],
122
            ];
123
        } else {
124
            array_push(
125
                $this->progressivePrice[$type]['thresholds'],
126
                [
127
                    'price' => $price,
128
                    'currency' => $currency,
129
                    'unit' => $unit,
130
                    'quantity' => $quantity,
131
                ]
132
            );
133
        }
134
    }
135
136
    /**
137
     * @Given /^build progressive price/
138
     */
139
    public function buildProgressivePrices()
140
    {
141
        foreach ($this->progressivePrice as $type => $price) {
142
            $type = new Type(Type::ANY, $type);
143
            $target = new Target(Target::ANY, $price['target']);
144
            $quantity = Quantity::create(Unit::create($price['unit']), 1);
0 ignored issues
show
Bug introduced by
hiqdev\php\units\Unit::create($price['unit']) of type hiqdev\php\units\Unit is incompatible with the type string expected by parameter $unit of hiqdev\php\units\AbstractQuantity::create(). ( Ignorable by Annotation )

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

144
            $quantity = Quantity::create(/** @scrutinizer ignore-type */ Unit::create($price['unit']), 1);
Loading history...
145
            $money = MoneyBuilder::buildMoney($price['price'], $price['currency']);
146
            $this->setPrice(new ProgressivePrice(null, $type, $target, $quantity, $money, $price['thresholds']));
147
        }
148
    }
149
150
    /**
151
     * @Given /sale close time is ([0-9.-]+)/
152
     */
153
    public function setActionCloseTime($closeTime): void
154
    {
155
        $this->sale->close(new DateTimeImmutable($closeTime));
156
    }
157
158
    private function setPrice($price)
159
    {
160
        $this->price = $price;
161
        $ref = new \ReflectionClass($this->plan);
162
        $prop = $ref->getProperty('prices');
163
        $prop->setAccessible(true);
164
        $prop->setValue($this->plan, [$price]);
165
    }
166
167
    /**
168
     * @Given /action is (\S+) ([\w_,]+)(?: ([0-9.]+) (\S+))?(?: in (.+))?/
169
     */
170
    public function actionIs(string $target, string $type, float $amount, string $unit, ?string $date = null): void
171
    {
172
        $type = new Type(Type::ANY, $type);
173
        $target = new Target(Target::ANY, $target);
174
        $time = new DateTimeImmutable($date);
175
        if ($this->sale->getCloseTime() instanceof DateTimeImmutable) {
176
            $amount = $amount * $this->getFractionOfMonth(
177
                $time, $time, $this->sale->getCloseTime()
178
            );
179
        }
180
        $quantity = Quantity::create($unit, $amount);
181
182
        $this->action = new Action(null, $type, $target, $quantity, $this->customer, $time);
183
    }
184
185
    private function getFractionOfMonth(DateTimeImmutable $month, DateTimeImmutable $startTime, DateTimeImmutable $endTime): float
186
    {
187
        // SQL function: days2quantity()
188
189
        $month = $month->modify('first day of this month 00:00');
190
        if ($startTime < $month) {
191
            $startTime = $month;
192
        }
193
        if ($endTime > $month->modify('first day of next month 00:00')) {
194
            $endTime = $month->modify('first day of next month 00:00');
195
        }
196
197
        $secondsInMonth = $month->format('t') * 24 * 60 * 60;
198
199
        return ($endTime->getTimestamp() - $startTime->getTimestamp()) / $secondsInMonth;
200
    }
201
202
    /**
203
     * @Given /formula is (.+)/
204
     */
205
    public function formulaIs(string $formula): void
206
    {
207
        $this->formula = $formula;
208
    }
209
210
    /**
211
     * @Given /formula continues (.+)/
212
     */
213
    public function formulaContinues(string $formula): void
214
    {
215
        $this->formula .= "\n" . $formula;
216
    }
217
218
    protected function getFormulaEngine()
219
    {
220
        if ($this->engine === null) {
221
            $this->engine = new FormulaEngine(new ArrayCachePool());
222
        }
223
224
        return $this->engine;
225
    }
226
227
    /**
228
     * @When /action date is (.+)/
229
     * @throws Exception
230
     */
231
    public function actionDateIs(string $date): void
232
    {
233
        $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

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

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

346
        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...
347
            'Charge type %s does not match expected %s', $type, $this->normalizeType($charge->getType()->getName())
348
        ));
349
        $money = $this->moneyParser->parse($sum, new Currency($currency));
0 ignored issues
show
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

349
        $money = $this->moneyParser->parse(/** @scrutinizer ignore-type */ $sum, new Currency($currency));
Loading history...
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

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

367
            /** @scrutinizer ignore-call */ 
368
            $storedEvents = $charge->releaseEvents();
Loading history...
368
            foreach (array_map('trim', explode(',', $events)) as $eventClass) {
369
                foreach ($storedEvents as $storedEvent) {
370
                    $eventReflection = new \ReflectionObject($storedEvent);
371
                    if ($eventReflection->getShortName() === $eventClass) {
372
                        continue 2;
373
                    }
374
                }
375
376
                Assert::fail(sprintf('Event of class %s is not present is charge', $eventClass));
377
            }
378
        } else {
379
            Assert::assertEmpty($charge->releaseEvents(), 'Failed asserting that charge does not have events');
380
        }
381
    }
382
383
    private function normalizeType($string): string
384
    {
385
        switch ($string) {
386
            case 'discount,discount':
387
                return 'discount';
388
            case 'monthly,leasing':
389
                return 'leasing';
390
            case 'monthly,installment':
391
                return 'installment';
392
            default:
393
                return $string;
394
        }
395
    }
396
397
    private function ensureNo(string $numeral): int
398
    {
399
        $formatter = new NumberFormatter('en_EN', NumberFormatter::SPELLOUT);
400
        $result = $formatter->parse($numeral);
401
        if ($result === false) {
402
            throw new Exception("Wrong numeral '$numeral'");
403
        }
404
405
        return --$result;
406
    }
407
}
408