Passed
Push — master ( 6e710f...a80202 )
by Andrii
12:46
created

BillingContext::targetHasTheFollowingUses()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 8
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 2
eloc 5
c 0
b 0
f 0
nc 2
nop 2
dl 0
loc 8
rs 10
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\Gherkin\Node\TableNode;
14
use BehatExpectException\ExpectException;
15
use DateTimeImmutable;
16
use hiqdev\php\billing\bill\BillInterface;
17
use hiqdev\php\billing\charge\ChargeInterface;
18
use PHPUnit\Framework\Assert;
19
20
class BillingContext extends BaseContext
21
{
22
    use ExpectException;
23
24
    protected $saleTime;
25
26
    protected $bill;
27
28
    protected $charges = [];
29
30
    /**
31
     * @Given reseller :reseller
32
     */
33
    public function reseller($reseller)
34
    {
35
        $this->builder->buildReseller($reseller);
36
    }
37
38
    /**
39
     * @Given customer :customer
40
     */
41
    public function customer($customer)
42
    {
43
        $this->builder->buildCustomer($customer);
44
    }
45
46
    /**
47
     * @Given manager :manager
48
     */
49
    public function manager($manager)
50
    {
51
        $this->builder->buildManager($manager);
0 ignored issues
show
Bug introduced by
The method buildManager() does not exist on hiqdev\php\billing\tests...tstrap\BuilderInterface. ( Ignorable by Annotation )

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

51
        $this->builder->/** @scrutinizer ignore-call */ 
52
                        buildManager($manager);

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...
52
    }
53
54
    /**
55
     * @Given /^(\S+ )?(\S+) tariff plan (\S+)/
56
     */
57
    public function plan($prefix, $type, $plan)
58
    {
59
        $prefix = strtr($prefix, ' ', '_');
60
        $grouping = $prefix === 'grouping_';
61
        $type = $grouping ? $type : $prefix.$type;
62
        $this->builder->buildPlan($plan, $type, $grouping);
63
    }
64
65
    protected function fullPrice(array $data)
66
    {
67
        if (!empty($data['price'])) {
68
            $data['rate'] = $data['price'];
69
        }
70
        $this->builder->buildPrice($data);
71
    }
72
73
    /**
74
     * @Given /price for (\S+) is +(\S+) (\S+) per (\S+) for target (.+)$/
75
     */
76
    public function priceWithTarget($type, $price, $currency, $unit, $target)
77
    {
78
        return $this->fullPrice(compact('type', 'price', 'currency', 'unit', 'target'));
0 ignored issues
show
Bug introduced by
Are you sure the usage of $this->fullPrice(compact...cy', 'unit', 'target')) targeting hiqdev\php\billing\tests...ingContext::fullPrice() seems to always return null.

This check looks for function or method calls that always return null and whose return value is used.

class A
{
    function getObject()
    {
        return null;
    }

}

$a = new A();
if ($a->getObject()) {

The method getObject() can return nothing but null, so it makes no sense to use the return value.

The reason is most likely that a function or method is imcomplete or has been reduced for debug purposes.

Loading history...
79
    }
80
81
    /**
82
     * @Given /price for (\S+) is +(\S+) (\S+) per (\S+) prepaid (\S+)$/
83
     */
84
    public function priceWithPrepaid($type, $price, $currency, $unit, $prepaid)
85
    {
86
        return $this->fullPrice(compact('type', 'price', 'currency', 'unit', 'prepaid'));
0 ignored issues
show
Bug introduced by
Are you sure the usage of $this->fullPrice(compact...y', 'unit', 'prepaid')) targeting hiqdev\php\billing\tests...ingContext::fullPrice() seems to always return null.

This check looks for function or method calls that always return null and whose return value is used.

class A
{
    function getObject()
    {
        return null;
    }

}

$a = new A();
if ($a->getObject()) {

The method getObject() can return nothing but null, so it makes no sense to use the return value.

The reason is most likely that a function or method is imcomplete or has been reduced for debug purposes.

Loading history...
87
    }
88
89
    /**
90
     * @Given /price for (\S+) is +(\S+) (\S+) per (\S+) prepaid (\S+) for target (\S+)$/
91
     */
92
    public function priceWithPrepaidAndTarget($type, $price, $currency, $unit, $prepaid, $target)
93
    {
94
        return $this->fullPrice(compact('type', 'price', 'currency', 'unit', 'prepaid', 'target'));
0 ignored issues
show
Bug introduced by
Are you sure the usage of $this->fullPrice(compact..., 'prepaid', 'target')) targeting hiqdev\php\billing\tests...ingContext::fullPrice() seems to always return null.

This check looks for function or method calls that always return null and whose return value is used.

class A
{
    function getObject()
    {
        return null;
    }

}

$a = new A();
if ($a->getObject()) {

The method getObject() can return nothing but null, so it makes no sense to use the return value.

The reason is most likely that a function or method is imcomplete or has been reduced for debug purposes.

Loading history...
95
    }
96
97
    /**
98
     * @Given /price for (\S+) is +(\S+) (\S+) per (\S+)$/
99
     */
100
    public function price($type, $price, $currency, $unit)
101
    {
102
        return $this->fullPrice(compact('type', 'price', 'currency', 'unit'));
0 ignored issues
show
Bug introduced by
Are you sure the usage of $this->fullPrice(compact...', 'currency', 'unit')) targeting hiqdev\php\billing\tests...ingContext::fullPrice() seems to always return null.

This check looks for function or method calls that always return null and whose return value is used.

class A
{
    function getObject()
    {
        return null;
    }

}

$a = new A();
if ($a->getObject()) {

The method getObject() can return nothing but null, so it makes no sense to use the return value.

The reason is most likely that a function or method is imcomplete or has been reduced for debug purposes.

Loading history...
103
    }
104
105
    /**
106
     * @Given /price for (\S+) is +(\S+) (\S+) per 1 (\S+) and (\S+) (\S+) per 2 (\S+) for target (\S+)/
107
     */
108
    public function enumPrice($type, $price, $currency, $unit, $price2, $currency2, $unit2, $target)
0 ignored issues
show
Unused Code introduced by
The parameter $currency2 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

108
    public function enumPrice($type, $price, $currency, $unit, $price2, /** @scrutinizer ignore-unused */ $currency2, $unit2, $target)

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

108
    public function enumPrice($type, $price, $currency, $unit, $price2, $currency2, /** @scrutinizer ignore-unused */ $unit2, $target)

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...
109
    {
110
        $sums = [1 => $price, 2 => $price2];
111
112
        return $this->fullPrice(compact('type', 'sums', 'currency', 'unit', 'target'));
0 ignored issues
show
Bug introduced by
Are you sure the usage of $this->fullPrice(compact...cy', 'unit', 'target')) targeting hiqdev\php\billing\tests...ingContext::fullPrice() seems to always return null.

This check looks for function or method calls that always return null and whose return value is used.

class A
{
    function getObject()
    {
        return null;
    }

}

$a = new A();
if ($a->getObject()) {

The method getObject() can return nothing but null, so it makes no sense to use the return value.

The reason is most likely that a function or method is imcomplete or has been reduced for debug purposes.

Loading history...
113
    }
114
115
    /**
116
     * @Given /^remove and recreate tariff plan (\S+)/
117
     */
118
    public function recreatePlan($plan)
119
    {
120
        $this->builder->recreatePlan($plan);
121
    }
122
123
    /**
124
     * @Given /sale target (\S+) by plan (\S+) at (\S+)/
125
     */
126
    public function sale($target, $plan, $time): void
127
    {
128
        $this->saleTime = $this->prepareTime($time);
129
        $this->builder->buildSale($target, $plan, $this->saleTime);
0 ignored issues
show
Bug introduced by
It seems like $this->saleTime can also be of type null; however, parameter $time of hiqdev\php\billing\tests...rInterface::buildSale() 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

129
        $this->builder->buildSale($target, $plan, /** @scrutinizer ignore-type */ $this->saleTime);
Loading history...
130
    }
131
132
    /**
133
     * @Given /purchase target (\S+) by plan (\S+) at (\S+)$/
134
     */
135
    public function purchaseTarget(string $target, string $plan, string $time): void
136
    {
137
        $time = $this->prepareTime($time);
138
        $this->builder->buildPurchase($target, $plan, $time);
139
    }
140
141
    /**
142
     * @Given /^purchase target "([^"]*)" by plan "([^"]*)" at "([^"]*)" with the following initial uses:$/
143
     */
144
    public function purchaseTargetWithInitialUses(string $target, string $plan, string $time, TableNode $usesTable): void
145
    {
146
        $time = $this->prepareTime($time);
147
        $uses = array_map(static function (array $row) {
148
            return [
149
                'type' => $row['type'],
150
                'unit' => $row['unit'],
151
                'amount' => $row['amount'],
152
            ];
153
        }, $usesTable->getColumnsHash());
154
155
        $this->mayFail(
156
            fn() => $this->builder->buildPurchase($target, $plan, $time, $uses)
157
        );
158
    }
159
160
    /**
161
     * @Given /resource consumption for (\S+) is +(\S+) (\S+) for target (\S+) at (.+)$/
162
     */
163
    public function setConsumption(string $type, int $amount, string $unit, string $target, string $time): void
164
    {
165
        $time = $this->prepareTime($time);
166
        $this->builder->setConsumption($type, $amount, $unit, $target, $time);
0 ignored issues
show
Bug introduced by
The method setConsumption() does not exist on hiqdev\php\billing\tests...tstrap\BuilderInterface. Since it exists in all sub-types, consider adding an abstract or default implementation to hiqdev\php\billing\tests...tstrap\BuilderInterface. ( Ignorable by Annotation )

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

166
        $this->builder->/** @scrutinizer ignore-call */ 
167
                        setConsumption($type, $amount, $unit, $target, $time);
Loading history...
167
    }
168
169
    /**
170
     * @Given /perform billing at (\S+)/
171
     */
172
    public function performBilling(string $time): void
173
    {
174
        $this->builder->performBilling($this->prepareTime($time));
175
    }
176
177
    /**
178
     * @Given /action for (\S+) is +(\S+) (\S+) +for target (.+?)( +at (\S+))?$/
179
     */
180
    public function setAction(string $type, int $amount, string $unit, string $target, string $at = null, string $time = null): void
0 ignored issues
show
Unused Code introduced by
The parameter $at 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

180
    public function setAction(string $type, int $amount, string $unit, string $target, /** @scrutinizer ignore-unused */ string $at = null, string $time = null): 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...
181
    {
182
        $time = $this->prepareTime($time);
183
        $this->builder->setAction($type, $amount, $unit, $target, $time);
0 ignored issues
show
Bug introduced by
It seems like $time can also be of type null; however, parameter $time of hiqdev\php\billing\tests...rInterface::setAction() 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

183
        $this->builder->setAction($type, $amount, $unit, $target, /** @scrutinizer ignore-type */ $time);
Loading history...
184
    }
185
186
    /**
187
     * @Given /perform calculation( at (\S+))?/
188
     */
189
    public function performCalculation(string $at = null, string $time = null): array
0 ignored issues
show
Unused Code introduced by
The parameter $at 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

189
    public function performCalculation(/** @scrutinizer ignore-unused */ string $at = null, string $time = null): array

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...
190
    {
191
        $this->charges = $this->builder->performCalculation($this->prepareTime($time));
0 ignored issues
show
Bug introduced by
It seems like $this->prepareTime($time) can also be of type null; however, parameter $time of hiqdev\php\billing\tests...e::performCalculation() 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

191
        $this->charges = $this->builder->performCalculation(/** @scrutinizer ignore-type */ $this->prepareTime($time));
Loading history...
192
        return $this->charges;
193
    }
194
195
    /**
196
     * @Given /bill +for (\S+) is +(\S+) (\S+) per (\S+) (\S+) for target (.+?)( +at (.+))?$/
197
     */
198
    public function billWithTime($type, $sum, $currency, $quantity, $unit, $target, $at = null, $time = null)
0 ignored issues
show
Unused Code introduced by
The parameter $at 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

198
    public function billWithTime($type, $sum, $currency, $quantity, $unit, $target, /** @scrutinizer ignore-unused */ $at = null, $time = null)

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...
199
    {
200
        $quantity = $this->prepareQuantity($quantity);
201
        $sum = $this->prepareSum($sum, $quantity);
202
        $time = $this->prepareTime($time);
203
        $bill = $this->findBill([
204
            'type' => $type,
205
            'target' => $target,
206
            'sum' => "$sum $currency",
207
            'quantity' => "$quantity $unit",
208
            'time' => $time,
209
        ]);
210
        Assert::assertSame($type, $bill->getType()->getName());
211
        Assert::assertSame($target, $bill->getTarget()->getFullName());
212
        Assert::assertEquals($sum * 100, $bill->getSum()->getAmount());
213
        Assert::assertSame($currency, $bill->getSum()->getCurrency()->getCode());
214
        Assert::assertEquals((float)$quantity, (float)$bill->getQuantity()->getQuantity());
215
        Assert::assertEquals(strtolower($unit), strtolower($bill->getQuantity()->getUnit()->getName()));
216
        if ($time) {
217
            Assert::assertEquals(new DateTimeImmutable($time), $bill->getTime());
218
        }
219
    }
220
221
    public function findBill(array $params): BillInterface
222
    {
223
        $bills = $this->builder->findBills($params);
224
        $this->bill = reset($bills);
225
        $this->charges = $this->bill->getCharges();
226
227
        return $this->bill;
228
    }
229
230
    /**
231
     * @Given /bills number is (\d+) for (\S+) for target (.+?)( +at (\S+))?$/
232
     */
233
    public function billsNumberWithTime($number, $type, $target, $at = null, $time = null)
0 ignored issues
show
Unused Code introduced by
The parameter $at 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

233
    public function billsNumberWithTime($number, $type, $target, /** @scrutinizer ignore-unused */ $at = null, $time = null)

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...
234
    {
235
        $count = count($this->builder->findBills(array_filter([
236
            'type' => $type,
237
            'target' => $target,
238
            'time' => $this->prepareTime($time),
239
        ])));
240
241
        Assert::assertEquals($number, $count);
242
    }
243
244
    /**
245
     * @Given /charges number is (\d+)/
246
     */
247
    public function chargesNumber($number)
248
    {
249
        Assert::assertEquals($number, count($this->charges));
250
    }
251
252
    /**
253
     * @Given /charge for (\S+) is +(\S+) (\S+) per (\S+) (\S+)$/
254
     */
255
    public function charge($type, $amount, $currency, $quantity, $unit)
256
    {
257
        $this->chargeWithTarget($type, $amount, $currency, $quantity, $unit, null);
258
    }
259
260
    /**
261
     * @Given /charge for (\S+) is +(\S+) (\S+) per +(\S+) (\S+) +for target (.+?)( +at (\S+))?$/
262
     */
263
    public function chargeWithTarget($type, $amount, $currency, $quantity, $unit, $target, $at = null, $time = null)
0 ignored issues
show
Unused Code introduced by
The parameter $time 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

263
    public function chargeWithTarget($type, $amount, $currency, $quantity, $unit, $target, $at = null, /** @scrutinizer ignore-unused */ $time = null)

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

263
    public function chargeWithTarget($type, $amount, $currency, $quantity, $unit, $target, /** @scrutinizer ignore-unused */ $at = null, $time = null)

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...
264
    {
265
        $quantity = $this->prepareQuantity($quantity);
266
        $amount = $this->prepareSum($amount, $quantity);
267
        $charge = $this->findCharge($type, $target);
268
        Assert::assertNotNull($charge);
269
        Assert::assertSame($type, $charge->getType()->getName());
270
        Assert::assertSame($target, $charge->getTarget()->getFullName());
271
        Assert::assertEquals($amount * 100, $charge->getSum()->getAmount());
272
        Assert::assertSame($currency, $charge->getSum()->getCurrency()->getCode());
273
        Assert::assertEquals((float)$quantity, (float)$charge->getUsage()->getQuantity());
274
        Assert::assertEquals(strtolower($unit), strtolower($charge->getUsage()->getUnit()->getName()));
275
    }
276
277
    public function findCharge($type, $target): ?ChargeInterface
278
    {
279
        foreach ($this->charges as $charge) {
280
            if ($charge->getType()->getName() !== $type) {
281
                continue;
282
            }
283
            if ($charge->getTarget()->getFullName() !== $target) {
284
                continue;
285
            }
286
287
            return $charge;
288
        }
289
290
        return null;
291
    }
292
293
    public function getNextCharge(): ChargeInterface
294
    {
295
        $charge = current($this->charges);
296
        next($this->charges);
297
298
        return $charge;
299
    }
300
301
    /**
302
     * @return string|false|null
303
     */
304
    protected function prepareTime(string $time = null)
305
    {
306
        if ($time === null) {
307
            return null;
308
        }
309
310
        if ($time === 'midnight second day of this month') {
311
            return date('Y-m-02');
312
        }
313
        if (strncmp($time, 'Y', 1) === 0) {
314
            return date($time);
315
        }
316
317
        return $time;
318
    }
319
320
    private function prepareQuantity($quantity)
321
    {
322
        if ($quantity[0] === 's') {
323
            return $this->getSaleQuantity();
324
        }
325
326
        return $quantity;
327
    }
328
329
    private function prepareSum($sum, $quantity)
330
    {
331
        if ($sum[0] === 's') {
332
            $sum = round(substr($sum, 1) * $quantity*100)/100;
333
        }
334
335
        return $sum;
336
    }
337
338
    public function getSaleQuantity()
339
    {
340
        return $this->days2quantity(new DateTimeImmutable($this->saleTime));
341
    }
342
343
    private function days2quantity(DateTimeImmutable $from)
344
    {
345
        $till = new DateTimeImmutable('first day of next month midnight');
346
        $diff = $from->diff($till);
347
        if ($diff->m) {
348
            return 1;
349
        }
350
351
        return $diff->d/date('t');
352
    }
353
354
    /**
355
     * @When /^tariff plan change is requested for target "([^"]*)" to plan "([^"]*)" at "([^"]*)"$/
356
     */
357
    public function tariffPlanChangeIsRequestedForTarget(string $target, string $planName, string $date)
358
    {
359
        $this->mayFail(fn () => $this->builder->targetChangePlan($target, $planName, $this->prepareTime($date)));
360
    }
361
362
    /**
363
     * @When /^tariff plan change is requested for target "([^"]*)" to plan "([^"]*)" at "([^"]*)", assuming current time is "([^"]*)"$/
364
     */
365
    public function tariffPlanChangeIsRequestedForTargetAtSpecificTime(string $target, string $planName, string $date, ?string $wallTime = null)
366
    {
367
        $this->mayFail(fn () => $this->builder->targetChangePlan($target, $planName, $this->prepareTime($date), $this->prepareTime($wallTime)));
368
    }
369
370
    /**
371
     * @Then /^target "([^"]*)" is sold to customer by plan "([^"]*)" since "([^"]*)"(?: till "([^"]*)")?$/
372
     */
373
    public function targetIsSoldToCustomerByPlanSinceTill(string $target, string $planName, string $saleDate, ?string $saleCloseDate = null)
374
    {
375
        $sales = $this->builder->findHistoricalSales([
376
            'target' => $target,
377
        ]);
378
379
        $saleDateTime = new DateTimeImmutable($saleDate);
380
        $saleCloseDateTime = new DateTimeImmutable($saleCloseDate);
381
382
        foreach ($sales as $sale) {
383
            /** @noinspection PhpBooleanCanBeSimplifiedInspection */
384
            $saleExists = true
385
                && str_contains($sale->getPlan()->getName(), $planName)
386
                && $sale->getTime()->format(DATE_ATOM) === $saleDateTime->format(DATE_ATOM)
387
                && (
388
                    ($saleCloseDate === null && $sale->getCloseTime() === null)
389
                    ||
390
                    ($saleCloseDate !== null && $sale->getCloseTime()->format(DATE_ATOM) === $saleCloseDateTime->format(DATE_ATOM))
391
                );
392
393
            if ($saleExists) {
394
                return;
395
            }
396
        }
397
398
        Assert::fail('Requested sale does not exist');
399
    }
400
401
    /**
402
     * @Then /^target "([^"]*)" has exactly (\d+) sales for customer$/
403
     */
404
    public function targetHasExactlySalesForCustomer(string $target, int $count)
405
    {
406
        $sales = $this->builder->findHistoricalSales([
407
            'target' => $target,
408
        ]);
409
410
        Assert::assertCount($count, $sales);
411
    }
412
413
    /**
414
     * @Then /^caught error is "([^"]*)"$/
415
     */
416
    public function caughtErrorIs(string $errorMessage): void
417
    {
418
        $this->assertCaughtExceptionMatches(\Throwable::class, $errorMessage);
419
    }
420
421
    /**
422
     * @Given /^target "([^"]*)"$/
423
     */
424
    public function target(string $target)
425
    {
426
        $this->builder->buildTarget($target);
427
    }
428
429
    /**
430
     * @Then /^flush entities cache$/
431
     */
432
    public function flushEntitiesCache()
433
    {
434
        $this->builder->flushEntitiesCache();
435
    }
436
437
    /**
438
     * @Given /^target "([^"]*)" has the following uses:$/
439
     */
440
    public function targetHasTheFollowingUses(string $target, TableNode $usesTable)
441
    {
442
        foreach ($usesTable->getColumnsHash() as $row) {
443
            $uses = $this->builder->findUsage($row['time'], $target, $row['type']);
444
            Assert::assertCount(1, $uses);
445
446
            $use = reset($uses);
447
            Assert::assertEquals($row['amount'], $use['total']);
448
        }
449
    }
450
}
451