Passed
Pull Request — master (#67)
by
unknown
12:51
created

FeatureContext::chargeIs()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 7
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 1
Metric Value
cc 2
eloc 4
c 1
b 0
f 1
nc 2
nop 8
dl 0
loc 7
rs 10

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

113
    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...
114
    {
115
        if (empty($this->progressivePrice[$type])) {
116
            $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...
117
                'target' => $target,
118
                'unit' => $unit,
119
                'condition' =>[
120
                    [
121
                        'price' => $price,
122
                        'currency' => $currency,
123
                        'sign_till' => self::PROGRESSIVE_SIGNS[$sign],
124
                        'value_till' => $quantity,
125
                    ],
126
                ] ,
127
            ];
128
        } else {
129
            array_push(
130
                $this->progressivePrice[$type]['condition'],
131
                [
132
                    'price' => $price,
133
                    'currency' => $currency,
134
                    'sign_till' => self::PROGRESSIVE_SIGNS[$sign],
135
                    'value_till' => $quantity,
136
                ]
137
            );
138
        }
139
    }
140
141
    /**
142
     * @Given /(\S+) progressive price for (\S+) is +(\S+) (\S+) per (\S+) (\S+) (\S+) (\S+) and (\S+) (\S+) (\S+)$/
143
     */
144
    public function progressivePriceWithInterval(
145
        $target,
146
        $type,
147
        $price,
148
        $currency,
149
        $unit,
150
        $signFrom,
151
        $quantityFrom,
152
        $perUnit,
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

152
        /** @scrutinizer ignore-unused */ $perUnit,

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...
153
        $signTill,
154
        $quantityTill,
155
        $perUnit1
0 ignored issues
show
Unused Code introduced by
The parameter $perUnit1 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

155
        /** @scrutinizer ignore-unused */ $perUnit1

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...
156
    ) {
157
        if (empty($this->progressivePrice[$type])) {
158
            $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...
159
                'target' => $target,
160
                'unit' => $unit,
161
                'condition' =>[
162
                    [
163
                        'price' => $price,
164
                        'currency' => $currency,
165
                        'sign_from' => self::PROGRESSIVE_SIGNS[$signFrom],
166
                        'value_from' => $quantityFrom,
167
                        'sign_till' => self::PROGRESSIVE_SIGNS[$signTill],
168
                        'value_till' => $quantityTill,
169
                    ],
170
                ] ,
171
            ];
172
        } else {
173
            array_push(
174
                $this->progressivePrice[$type]['condition'],
175
                [
176
                    'price' => $price,
177
                    'currency' => $currency,
178
                    'sign_from' => self::PROGRESSIVE_SIGNS[$signFrom],
179
                    'value_from' => $quantityFrom,
180
                    'sign_till' => self::PROGRESSIVE_SIGNS[$signTill],
181
                    'value_till' => $quantityTill,
182
                ]
183
            );
184
        }
185
    }
186
187
    /**
188
     * @Given /^create progressive price/
189
     */
190
    public function createProgressivePrices()
191
    {
192
        foreach ($this->progressivePrice as $type => $price) {
193
            $type = new Type(Type::ANY, $type);
194
            $target = new Target(Target::ANY, $price['target']);
195
            $quantity = Quantity::create($price['unit'], 1);
196
            $this->setPrice(new ProgressivePrice(null, $type, $target, $quantity, $price['condition']));
197
        }
198
    }
199
200
    /**
201
     * @Given /sale close time is ([0-9.-]+)/
202
     */
203
    public function setActionCloseTime($closeTime): void
204
    {
205
        $this->sale->close(new DateTimeImmutable($closeTime));
206
    }
207
208
    private function setPrice($price)
209
    {
210
        $this->price = $price;
211
        $ref = new \ReflectionClass($this->plan);
212
        $prop = $ref->getProperty('prices');
213
        $prop->setAccessible(true);
214
        $prop->setValue($this->plan, [$price]);
215
    }
216
217
    /**
218
     * @Given /action is (\S+) ([\w_,]+)(?: ([0-9.]+) (\S+))?(?: in (.+))?/
219
     */
220
    public function actionIs(string $target, string $type, float $amount, string $unit, ?string $date = null): void
221
    {
222
        $type = new Type(Type::ANY, $type);
223
        $target = new Target(Target::ANY, $target);
224
        $time = new DateTimeImmutable($date);
225
        if ($this->sale->getCloseTime() instanceof DateTimeImmutable) {
226
            $amount = $amount * $this->getFractionOfMonth(
227
                $time, $time, $this->sale->getCloseTime()
228
            );
229
        }
230
        $quantity = Quantity::create($unit, $amount);
231
232
        $this->action = new Action(null, $type, $target, $quantity, $this->customer, $time);
233
    }
234
235
    private function getFractionOfMonth(DateTimeImmutable $month, DateTimeImmutable $startTime, DateTimeImmutable $endTime): float
236
    {
237
        // SQL function: days2quantity()
238
239
        $month = $month->modify('first day of this month 00:00');
240
        if ($startTime < $month) {
241
            $startTime = $month;
242
        }
243
        if ($endTime > $month->modify('first day of next month 00:00')) {
244
            $endTime = $month->modify('first day of next month 00:00');
245
        }
246
247
        $secondsInMonth = $month->format('t') * 24 * 60 * 60;
248
249
        return ($endTime->getTimestamp() - $startTime->getTimestamp()) / $secondsInMonth;
250
    }
251
252
    /**
253
     * @Given /formula is (.+)/
254
     */
255
    public function formulaIs(string $formula): void
256
    {
257
        $this->formula = $formula;
258
    }
259
260
    /**
261
     * @Given /formula continues (.+)/
262
     */
263
    public function formulaContinues(string $formula): void
264
    {
265
        $this->formula .= "\n" . $formula;
266
    }
267
268
    protected function getFormulaEngine()
269
    {
270
        if ($this->engine === null) {
271
            $this->engine = new FormulaEngine(new ArrayCachePool());
272
        }
273
274
        return $this->engine;
275
    }
276
277
    /**
278
     * @When /action date is (.+)/
279
     * @throws Exception
280
     */
281
    public function actionDateIs(string $date): void
282
    {
283
        $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

283
        $this->action->/** @scrutinizer ignore-call */ 
284
                       setTime(new DateTimeImmutable($date));
Loading history...
284
    }
285
286
    /**
287
     * @Given /^client rejected service at (.+)$/
288
     */
289
    public function actionCloseDateIs(string $close_date): void
290
    {
291
        $this->sale->close(new DateTimeImmutable($close_date));
292
    }
293
294
    /**
295
     * @Then /^error is$/m
296
     */
297
    public function multilineErrorIs(\Behat\Gherkin\Node\PyStringNode $value)
298
    {
299
        $this->expectedError = $value->getRaw();
300
    }
301
302
    /**
303
     * @Then /^error is (.+)$/
304
     *
305
     * @param string $error
306
     */
307
    public function errorIs($error): void
308
    {
309
        $this->expectedError = $error;
310
    }
311
312
    /**
313
     * @Then /^(\w+) charge is ?(?: with ?)?$/
314
     */
315
    public function emptyCharge(string $numeral): void
316
    {
317
        $this->chargeIs($numeral);
318
    }
319
320
    /**
321
     * @Then /^(\w+) charge is (\S+) +(-?[0-9.]+) ([A-Z]{3})(?: for ([\d.]+)? (\w+)?)?(?: with (.+)?)?$/
322
     */
323
    public function chargeWithSum($numeral, $type = null, $sum = null, $currency = null, $qty = null, $unit = null, $events = null): void
324
    {
325
        $this->chargeIs($numeral, $type, $sum, $currency, null, $qty, $unit, $events);
326
    }
327
328
    /**
329
     * @Then /^(\w+) charge is (\S+) +(-?[0-9.]+) ([A-Z]{3}) reason ([\w]+)(?: with (.+)?)?$/
330
     */
331
    public function chargeWithReason($numeral, $type = null, $sum = null, $currency = null, $reason = null, $events = null): void
332
    {
333
        $this->chargeIs($numeral, $type, $sum, $currency, $reason, null, null, $events);
334
    }
335
336
    public function chargeIs($numeral, $type = null, $sum = null, $currency = null, $reason = null, $qty = null, $unit = null, $events = null): void
337
    {
338
        $no = $this->ensureNo($numeral);
339
        if ($no === 0) {
340
            $this->calculatePrice();
341
        }
342
        $this->assertCharge($this->charges[$no] ?? null, $type, $sum, $currency, $reason, $qty, $unit, $events);
343
    }
344
345
    /**
346
     * @When /^calculating charges$/
347
     */
348
    public function calculatePrice(): void
349
    {
350
        $this->expectError(function () {
351
            if ($this->formula !== null) {
352
                $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

352
                $this->price->/** @scrutinizer ignore-call */ 
353
                              setModifier($this->getFormulaEngine()->build($this->formula));
Loading history...
353
            }
354
            $this->charges = $this->billing->calculateCharges($this->action);
355
        });
356
    }
357
358
    public function expectError(Closure $closure): void
359
    {
360
        try {
361
            call_user_func($closure);
362
        } catch (Exception $e) {
363
            if ($this->isExpectedError($e)) {
364
                $this->expectedError = null;
365
            } else {
366
                throw $e;
367
            }
368
        }
369
        if ($this->expectedError) {
370
            throw new Exception('failed receive expected exception');
371
        }
372
    }
373
374
    protected function isExpectedError(Exception $e): bool
375
    {
376
        return str_starts_with($e->getMessage(), $this->expectedError);
377
    }
378
379
    /**
380
     * @param ChargeInterface|Charge|null $charge
381
     * @param string|null $type
382
     * @param string|null $sum
383
     * @param string|null $currency
384
     * @param string|null $reason
385
     * @param string|null $qty
386
     * @param string|null $unit
387
     * @param string|null $events
388
     */
389
    public function assertCharge($charge, $type, $sum, $currency, $reason, $qty, $unit, $events): void
390
    {
391
        if (empty($type) && empty($sum) && empty($currency)) {
392
            is_null($charge) || Assert::assertSame('0', $charge->getSum()->getAmount());
393
            return;
394
        }
395
        Assert::assertInstanceOf(Charge::class, $charge);
396
        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

396
        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...
397
            'Charge type %s does not match expected %s', $type, $this->normalizeType($charge->getType()->getName())
398
        ));
399
        $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

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

399
        $money = $this->moneyParser->parse(/** @scrutinizer ignore-type */ $sum, new Currency($currency));
Loading history...
400
        Assert::assertTrue($money->equals($charge->getSum()), sprintf(
401
            'Charge sum %s does not match expected %s', $charge->getSum()->getAmount(), $money->getAmount()
402
        ));
403
        if ($reason !== null) {
404
            Assert::assertSame($reason, $charge->getComment(),
405
                sprintf('Charge comment %s does not match expected %s', $charge->getComment(), $reason)
406
            );
407
        }
408
        if ($qty !== null && $unit !== null) {
409
            Assert::assertEqualsWithDelta($qty, $charge->getUsage()->getQuantity(), 1e-7,
410
                sprintf('Charge quantity "%s" does not match expected "%s"', $charge->getUsage()->getQuantity(), $qty)
411
            );
412
            Assert::assertSame($unit, $charge->getUsage()->getUnit()->getName(),
413
                sprintf('Charge unit "%s" does not match expected "%s"', $charge->getUsage()->getUnit()->getName(), $unit)
414
            );
415
        }
416
        if ($events !== null) {
417
            $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

417
            /** @scrutinizer ignore-call */ 
418
            $storedEvents = $charge->releaseEvents();
Loading history...
418
            foreach (array_map('trim', explode(',', $events)) as $eventClass) {
419
                foreach ($storedEvents as $storedEvent) {
420
                    $eventReflection = new \ReflectionObject($storedEvent);
421
                    if ($eventReflection->getShortName() === $eventClass) {
422
                        continue 2;
423
                    }
424
                }
425
426
                Assert::fail(sprintf('Event of class %s is not present is charge', $eventClass));
427
            }
428
        } else {
429
            Assert::assertEmpty($charge->releaseEvents(), 'Failed asserting that charge does not have events');
430
        }
431
    }
432
433
    private function normalizeType($string): string
434
    {
435
        switch ($string) {
436
            case 'discount,discount':
437
                return 'discount';
438
            case 'monthly,leasing':
439
                return 'leasing';
440
            case 'monthly,installment':
441
                return 'installment';
442
            default:
443
                return $string;
444
        }
445
    }
446
447
    private function ensureNo(string $numeral): int
448
    {
449
        $formatter = new NumberFormatter('en_EN', NumberFormatter::SPELLOUT);
450
        $result = $formatter->parse($numeral);
451
        if ($result === false) {
452
            throw new Exception("Wrong numeral '$numeral'");
453
        }
454
455
        return --$result;
456
    }
457
}
458