Passed
Pull Request — master (#67)
by
unknown
14:21
created

FeatureContext::progressivePrice()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 25
Code Lines 18

Duplication

Lines 0
Ratio 0 %

Importance

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

105
    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

105
    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...
106
    {
107
        if (empty($this->progressivePrice[$type])) {
108
            $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...
109
                'target' => $target,
110
                'unit' => $unit,
111
                'price' => $price,
112
                'currency' => $currency,
113
                'thresholds' =>[
114
                    [
115
                        'price' => $price,
116
                        'currency' => $currency,
117
                        'unit' => $unit,
118
                        'quantity' => $quantity,
119
                    ]
120
                ],
121
            ];
122
        } else {
123
            array_push(
124
                $this->progressivePrice[$type]['thresholds'],
125
                [
126
                    'price' => $price,
127
                    'currency' => $currency,
128
                    'unit' => $unit,
129
                    'quantity' => $quantity,
130
                ]
131
            );
132
        }
133
    }
134
135
    /**
136
     * @Given /^create progressive price/
137
     */
138
    public function createProgressivePrices()
139
    {
140
        foreach ($this->progressivePrice as $type => $price) {
141
            $type = new Type(Type::ANY, $type);
142
            $target = new Target(Target::ANY, $price['target']);
143
            $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

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

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

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

345
        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...
346
            'Charge type %s does not match expected %s', $type, $this->normalizeType($charge->getType()->getName())
347
        ));
348
        $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

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

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

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