Passed
Pull Request — master (#67)
by
unknown
13:46
created

FeatureContext::progressivePrice()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 15
Code Lines 11

Duplication

Lines 0
Ratio 0 %

Importance

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

102
    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

102
    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...
103
    {
104
        if (empty($this->progressivePrice[$type])) {
105
            $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...
106
                'target' => $target,
107
                'unit' => $unit,
108
                'currency' => $currency,
109
                'thresholds' =>[
110
                    new ProgressivePriceThresholdsDto($price, new Currency($currency), $quantity)
111
                ],
112
            ];
113
        } else {
114
            array_push(
115
                $this->progressivePrice[$type]['thresholds'],
116
                new ProgressivePriceThresholdsDto($price, new Currency($currency), $quantity)
117
            );
118
        }
119
    }
120
121
    /**
122
     * @Given /^create progressive price/
123
     */
124
    public function createProgressivePrices()
125
    {
126
        foreach ($this->progressivePrice as $type => $price) {
127
            $type = new Type(Type::ANY, $type);
128
            $target = new Target(Target::ANY, $price['target']);
129
            $quantity = Quantity::create($price['unit'], 1);
130
            $this->setPrice(new ProgressivePrice(null, $type, $target, $quantity, $price['thresholds']));
0 ignored issues
show
Bug introduced by
The call to hiqdev\php\billing\price...ivePrice::__construct() has too few arguments starting with thresholds. ( Ignorable by Annotation )

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

130
            $this->setPrice(/** @scrutinizer ignore-call */ new ProgressivePrice(null, $type, $target, $quantity, $price['thresholds']));

This check compares calls to functions or methods with their respective definitions. If the call has less arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress. Please note the @ignore annotation hint above.

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

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

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

330
        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...
331
            'Charge type %s does not match expected %s', $type, $this->normalizeType($charge->getType()->getName())
332
        ));
333
        $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

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

333
        $money = $this->moneyParser->parse(/** @scrutinizer ignore-type */ $sum, new Currency($currency));
Loading history...
334
        Assert::assertTrue($money->equals($charge->getSum()), sprintf(
335
            'Charge sum %s does not match expected %s', $charge->getSum()->getAmount(), $money->getAmount()
336
        ));
337
        if ($reason !== null) {
338
            Assert::assertSame($reason, $charge->getComment(),
339
                sprintf('Charge comment %s does not match expected %s', $charge->getComment(), $reason)
340
            );
341
        }
342
        if ($qty !== null && $unit !== null) {
343
            Assert::assertEqualsWithDelta($qty, $charge->getUsage()->getQuantity(), 1e-7,
344
                sprintf('Charge quantity "%s" does not match expected "%s"', $charge->getUsage()->getQuantity(), $qty)
345
            );
346
            Assert::assertSame($unit, $charge->getUsage()->getUnit()->getName(),
347
                sprintf('Charge unit "%s" does not match expected "%s"', $charge->getUsage()->getUnit()->getName(), $unit)
348
            );
349
        }
350
        if ($events !== null) {
351
            $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

351
            /** @scrutinizer ignore-call */ 
352
            $storedEvents = $charge->releaseEvents();
Loading history...
352
            foreach (array_map('trim', explode(',', $events)) as $eventClass) {
353
                foreach ($storedEvents as $storedEvent) {
354
                    $eventReflection = new \ReflectionObject($storedEvent);
355
                    if ($eventReflection->getShortName() === $eventClass) {
356
                        continue 2;
357
                    }
358
                }
359
360
                Assert::fail(sprintf('Event of class %s is not present is charge', $eventClass));
361
            }
362
        } else {
363
            Assert::assertEmpty($charge->releaseEvents(), 'Failed asserting that charge does not have events');
364
        }
365
    }
366
367
    private function normalizeType($string): string
368
    {
369
        switch ($string) {
370
            case 'discount,discount':
371
                return 'discount';
372
            case 'monthly,leasing':
373
                return 'leasing';
374
            case 'monthly,installment':
375
                return 'installment';
376
            default:
377
                return $string;
378
        }
379
    }
380
381
    private function ensureNo(string $numeral): int
382
    {
383
        $formatter = new NumberFormatter('en_EN', NumberFormatter::SPELLOUT);
384
        $result = $formatter->parse($numeral);
385
        if ($result === false) {
386
            throw new Exception("Wrong numeral '$numeral'");
387
        }
388
389
        return --$result;
390
    }
391
}
392