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

FeatureContext::multilineErrorIs()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 1
nc 1
nop 1
dl 0
loc 3
rs 10
c 0
b 0
f 0
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