Completed
Push — master ( d75f5b...e2e89f )
by Andrii
14:28
created

FeatureContext::ensureNo()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 9
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 2
eloc 5
nc 2
nop 1
dl 0
loc 9
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-2018, HiQDev (http://hiqdev.com/)
9
 */
10
11
namespace hiqdev\php\billing\tests\behat\bootstrap;
12
13
use Behat\Behat\Context\Context;
14
use Cache\Adapter\PHPArray\ArrayCachePool;
15
use Closure;
16
use DateTimeImmutable;
17
use hiqdev\php\billing\action\Action;
18
use hiqdev\php\billing\charge\Charge;
19
use hiqdev\php\billing\charge\ChargeInterface;
20
use hiqdev\php\billing\customer\Customer;
21
use hiqdev\php\billing\formula\FormulaEngine;
22
use hiqdev\php\billing\plan\Plan;
23
use hiqdev\php\billing\price\SinglePrice;
24
use hiqdev\php\billing\target\Target;
25
use hiqdev\php\billing\tests\support\order\SimpleBilling;
26
use hiqdev\php\billing\type\Type;
27
use hiqdev\php\units\Quantity;
28
use Money\Currencies\ISOCurrencies;
29
use Money\Parser\DecimalMoneyParser;
30
use NumberFormatter;
31
use PHPUnit\Framework\Assert;
32
33
/**
34
 * Defines application features from the specific context.
35
 */
36
class FeatureContext implements Context
37
{
38
    protected $engine;
39
40
    /** @var Customer */
41
    protected $customer;
42
    /**
43
     * @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...
44
     *
45
     * TODO: FormulaChargeModifierTrait::setFormula() must be moved to interface
46
     */
47
    protected $price;
48
49
    /** @var string */
50
    protected $formula;
51
52
    /**
53
     * @var \hiqdev\php\billing\action\ActionInterface|\hiqdev\php\billing\action\AbstractAction
54
     */
55
    protected $action;
56
    /**
57
     * @var ChargeInterface[]
58
     */
59
    protected $charges;
60
61
    /** @var \Money\MoneyParser */
62
    protected $moneyParser;
63
64
    /** @var string */
65
    protected $expectedError;
66
67
    /**
68
     * Initializes context.
69
     */
70
    public function __construct()
71
    {
72
        $this->customer = new Customer(null, 'somebody');
73
        $this->moneyParser = new DecimalMoneyParser(new ISOCurrencies());
74
        $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...
75
        $this->billing = SimpleBilling::fromPlan($this->plan);
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...
76
    }
77
78
    /**
79
     * @Given /(\S+) (\S+) price is ([0-9.]+) (\w+) per (\w+)(?: includes ([\d.]+))?/
80
     */
81
    public function priceIs($target, $type, $sum, $currency, $unit, $quantity = 0)
82
    {
83
        $type = new Type(Type::ANY, $type);
84
        $target = new Target(Target::ANY, $target);
85
        $quantity = Quantity::create($unit, $quantity);
86
        $sum = $this->moneyParser->parse($sum, $currency);
87
        $this->setPrice(new SinglePrice(null, $type, $target, null, $quantity, $sum));
88
    }
89
90
    private function setPrice($price)
91
    {
92
        $this->price = $price;
93
        $ref = new \ReflectionClass($this->plan);
94
        $prop = $ref->getProperty('prices');
95
        $prop->setAccessible(true);
96
        $prop->setValue($this->plan, [$price]);
97
    }
98
99
    /**
100
     * @Given /action is (\S+) ([\w_,]+) ([0-9.]+) (\S+)/
101
     */
102
    public function actionIs($target, $type, $amount, $unit)
103
    {
104
        $type = new Type(Type::ANY, $type);
105
        $target = new Target(Target::ANY, $target);
106
        $quantity = Quantity::create($unit, $amount);
107
        $time = new DateTimeImmutable();
108
        $this->action = new Action(null, $type, $target, $quantity, $this->customer, $time);
109
    }
110
111
    /**
112
     * @Given /formula is (.+)/
113
     * @param string $formula
114
     */
115
    public function formulaIs(string $formula): void
116
    {
117
        $this->formula = $formula;
118
    }
119
120
    /**
121
     * @Given /formula continues (.+)/
122
     * @param string $formula
123
     */
124
    public function formulaContinues(string $formula): void
125
    {
126
        $this->formula .= "\n" . $formula;
127
    }
128
129
    protected function getFormulaEngine()
130
    {
131
        if ($this->engine === null) {
132
            $this->engine = new FormulaEngine(new ArrayCachePool());
133
        }
134
135
        return $this->engine;
136
    }
137
138
    /**
139
     * @When /action date is ([0-9.-]+)/
140
     * @param string $date
141
     * @throws \Exception
142
     */
143
    public function actionDateIs(string $date): void
144
    {
145
        $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

145
        $this->action->/** @scrutinizer ignore-call */ 
146
                       setTime(new DateTimeImmutable($date));
Loading history...
146
    }
147
148
    /**
149
     * @Then /^error is$/m
150
     */
151
    public function multilineErrorIs(\Behat\Gherkin\Node\PyStringNode $value)
152
    {
153
        $this->expectedError = $value->getRaw();
154
    }
155
156
    /**
157
     * @Then /^error is (.+)$/
158
     *
159
     * @param string $error
160
     */
161
    public function errorIs($error): void
162
    {
163
        $this->expectedError = $error;
164
    }
165
166
    /**
167
     * @Then /^(\w+) charge is ?(?: with ?)?$/
168
     * @param string $numeral
169
     */
170
    public function emptyCharge(string $numeral): void
171
    {
172
        $this->chargeIs($numeral);
173
    }
174
175
    /**
176
     * @Then /^(\w+) charge is (\S+) +(-?[0-9.]+) ([A-Z]{3})(?: with (.+)?)?$/
177
     */
178
    public function chargeWithSum($numeral, $type = null, $sum = null, $currency = null, $events = null): void
179
    {
180
        $this->chargeIs($numeral, $type, $sum, $currency, null, $events);
181
    }
182
183
    /**
184
     * @Then /^(\w+) charge is (\S+) +(-?[0-9.]+) ([A-Z]{3}) reason ([\w]+)(?: with (.+)?)?$/
185
     */
186
    public function chargeWithReason($numeral, $type = null, $sum = null, $currency = null, $reason = null, $events = null): void
187
    {
188
        $this->chargeIs($numeral, $type, $sum, $currency, $reason, $events);
189
    }
190
191
    public function chargeIs($numeral, $type = null, $sum = null, $currency = null, $reason = null, $events = null): void
192
    {
193
        $no = $this->ensureNo($numeral);
194
        if ($no === 0) {
195
            $this->calculatePrice();
196
        }
197
        $this->assertCharge($this->charges[$no] ?? null, $type, $sum, $currency, $reason, $events);
198
    }
199
200
    /**
201
     * @When /^calculating charges$/
202
     */
203
    public function calculatePrice(): void
204
    {
205
        $this->expectError(function () {
206
            $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

206
            $this->price->/** @scrutinizer ignore-call */ 
207
                          setModifier($this->getFormulaEngine()->build($this->formula));
Loading history...
207
            $this->charges = $this->billing->calculateCharges($this->action);
208
        });
209
    }
210
211
    public function expectError(Closure $closure): void
212
    {
213
        try {
214
            call_user_func($closure);
215
        } catch (\Exception $e) {
216
            if ($this->isExpectedError($e)) {
217
                $this->expectedError = null;
218
            } else {
219
                throw $e;
220
            }
221
        }
222
        if ($this->expectedError) {
223
            throw new \Exception('failed receive expected exception');
224
        }
225
    }
226
227
    protected function isExpectedError(\Exception $e): bool
228
    {
229
        return $this->startsWith($e->getMessage(), $this->expectedError);
230
    }
231
232
    protected function startsWith(string $string, string $prefix = null): bool
233
    {
234
        return $prefix && strncmp($string, $prefix, strlen($prefix)) === 0;
235
    }
236
237
    /**
238
     * @param ChargeInterface|Charge|null $charge
239
     * @param string|null $type
240
     * @param string|null $sum
241
     * @param string|null $currency
242
     * @param string|null $reason
243
     * @param string|null $events
244
     */
245
    public function assertCharge($charge, $type, $sum, $currency, $reason, $events): void
246
    {
247
        if (empty($type) && empty($sum) && empty($currency)) {
248
            Assert::assertNull($charge);
249
250
            return;
251
        }
252
        Assert::assertInstanceOf(Charge::class, $charge);
253
        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

253
        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...
254
            'Charge type %s does not match expected %s', $type, $this->normalizeType($charge->getType()->getName())
255
        ));
256
        $money = $this->moneyParser->parse($sum, $currency);
257
        Assert::assertTrue($money->equals($charge->getSum()), sprintf(
258
            'Charge sum %s does not match expected %s', $charge->getSum()->getAmount(), $money->getAmount()
259
        ));
260
        if ($reason !== null) {
261
            Assert::assertSame($reason, $charge->getComment(),
262
                sprintf('Charge comment %s does not match expected %s', $charge->getComment(), $reason)
263
            );
264
        }
265
        if ($events !== null) {
266
            $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

266
            /** @scrutinizer ignore-call */ 
267
            $storedEvents = $charge->releaseEvents();
Loading history...
267
            foreach (array_map('trim', explode(',', $events)) as $eventClass) {
268
                foreach ($storedEvents as $storedEvent) {
269
                    $eventReflection = new \ReflectionObject($storedEvent);
270
                    if ($eventReflection->getShortName() === $eventClass) {
271
                        continue 2;
272
                    }
273
                }
274
275
                Assert::fail(sprintf('Event of class %s is not present is charge', $eventClass));
276
            }
277
        } else {
278
            Assert::assertEmpty($charge->releaseEvents(), 'Failed asserting that charge does not have events');
279
        }
280
    }
281
282
    private function normalizeType($string): string
283
    {
284
        switch ($string) {
285
            case 'discount,discount':
286
                return 'discount';
287
            case 'monthly,leasing';
288
                return 'leasing';
289
            default:
290
                return $string;
291
        }
292
    }
293
294
    private function ensureNo(string $numeral): int
295
    {
296
        $formatter = new NumberFormatter('en_EN', NumberFormatter::SPELLOUT);
297
        $result = $formatter->parse($numeral);
298
        if ($result === false) {
299
            throw new \Exception("Wrong numeral '$numeral'");
300
        }
301
302
        return --$result;
303
    }
304
}
305