Passed
Pull Request — master (#99)
by
unknown
02:46
created

BillingContext::prepareTime()   B

Complexity

Conditions 7
Paths 8

Size

Total Lines 25
Code Lines 15

Duplication

Lines 0
Ratio 0 %

Importance

Changes 5
Bugs 1 Features 0
Metric Value
eloc 15
c 5
b 1
f 0
dl 0
loc 25
rs 8.8333
cc 7
nc 8
nop 1
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
        mayFail as protected;
24
        shouldFail as protected;
25
        assertCaughtExceptionMatches as protected;
26
    }
27
28
    protected $saleTime;
29
30
    protected $bill;
31
32
    protected $charges = [];
33
34
    protected array $progressivePrice = [];
35
36
    /**
37
     * @Given reseller :reseller
38
     */
39
    public function reseller($reseller)
40
    {
41
        $this->builder->buildReseller($reseller);
42
    }
43
44
    /**
45
     * @Given customer :customer
46
     */
47
    public function customer($customer)
48
    {
49
        $this->builder->buildCustomer($customer);
50
    }
51
52
    /**
53
     * @Given manager :manager
54
     */
55
    public function manager($manager)
56
    {
57
        $this->builder->buildManager($manager);
58
    }
59
60
    /**
61
     * @Given /^(\S+ )?(\S+) tariff plan (\S+)/
62
     */
63
    public function plan($prefix, $type, $plan)
64
    {
65
        $prefix = strtr($prefix, ' ', '_');
66
        $grouping = $prefix === 'grouping_';
67
        $type = $grouping ? $type : $prefix.$type;
68
        $this->builder->buildPlan($plan, $type, $grouping);
69
    }
70
71
    protected function fullPrice(array $data)
72
    {
73
        if (!empty($data['price'])) {
74
            $data['rate'] = $data['price'];
75
        }
76
        $this->builder->buildPrice($data);
77
    }
78
79
    /**
80
     * @Given /price for (\S+) is +(\S+) (\S+) per (\S+) for target (.+)$/
81
     */
82
    public function priceWithTarget($type, $price, $currency, $unit, $target)
83
    {
84
        return $this->fullPrice(compact('type', 'price', 'currency', 'unit', 'target'));
85
    }
86
87
    /**
88
     * @Given /price for (\S+) is +(\S+) (\S+) per (\S+) prepaid (\S+)$/
89
     */
90
    public function priceWithPrepaid($type, $price, $currency, $unit, $prepaid)
91
    {
92
        return $this->fullPrice(compact('type', 'price', 'currency', 'unit', 'prepaid'));
93
    }
94
95
    /**
96
     * @Given /price for (\S+) is +(\S+) (\S+) per (\S+) prepaid (\S+) for target (\S+)$/
97
     */
98
    public function priceWithPrepaidAndTarget($type, $price, $currency, $unit, $prepaid, $target)
99
    {
100
        return $this->fullPrice(compact('type', 'price', 'currency', 'unit', 'prepaid', 'target'));
101
    }
102
103
    /**
104
     * @Given /price for (\S+) is +(\S+) (\S+) per (\S+)$/
105
     */
106
    public function price($type, $price, $currency, $unit)
107
    {
108
        return $this->fullPrice(compact('type', 'price', 'currency', 'unit'));
109
    }
110
111
    /**
112
     * @Given /price for (\S+) is +(\S+) (\S+) per 1 (\S+) and (\S+) (\S+) per 2 (\S+) for target (\S+)/
113
     */
114
    public function enumPrice($type, $price, $currency, $unit, $price2, $currency2, $unit2, $target)
115
    {
116
        $sums = [1 => $price, 2 => $price2];
117
118
        return $this->fullPrice(compact('type', 'sums', 'currency', 'unit', 'target'));
119
    }
120
121
    /**
122
     * @Given /progressive price for (\S+) is +(\S+) (\S+) per (\S+) (\S+) (\S+) (\S+)$/
123
     */
124
    public function progressivePrice($type, $price, $currency, $unit, $sign, $quantity, $perUnit): void
125
    {
126
        if (empty($this->progressivePrice[$type])) {
127
            $this->progressivePrice[$type] = [
128
                'price' => 0,
129
                'currency' => $currency,
130
                'unit' => $unit,
131
                'thresholds' =>[
132
                    [
133
                        'price' => $price,
134
                        'currency' => $currency,
135
                        'quantity' => $quantity,
136
                        'unit' => $unit,
137
                    ],
138
                ] ,
139
            ];
140
        } else {
141
            array_push(
142
                $this->progressivePrice[$type]['thresholds'],
143
                [
144
                    'price' => $price,
145
                    'currency' => $currency,
146
                    'quantity' => $quantity,
147
                    'unit' => $unit,
148
                ]
149
            );
150
        }
151
    }
152
153
    /**
154
     * @Given /^build progressive price/
155
     */
156
    public function buildProgressivePrices()
157
    {
158
        foreach ($this->progressivePrice as $type => $price) {
159
            $this->fullPrice([
160
                'type' => $type,
161
                'price' => 0,
162
                'currency' => $price['currency'],
163
                'unit' => $price['unit'],
164
                'data' => ['thresholds' => $price['thresholds'], 'class' => 'ProgressivePrice'],
165
            ]);
166
        }
167
    }
168
169
    /**
170
     * @Given /^remove and recreate tariff plan (\S+)/
171
     */
172
    public function recreatePlan($plan)
173
    {
174
        $this->builder->recreatePlan($plan);
175
    }
176
177
    /**
178
     * @Given /sale target (\S+) by plan (\S+) at (\S+)/
179
     */
180
    public function sale($target, $plan, $time): void
181
    {
182
        $this->saleTime = $this->prepareTime($time);
183
        $this->builder->buildSale($target, $plan, $this->saleTime);
184
    }
185
    /**
186
     * @When /^sale close is requested for target "([^"]*)" at "([^"]*)", assuming current time is "([^"]*)"$/
187
     */
188
    public function saleClose(string $target, string $time, ?string $wallTime)
189
    {
190
        throw new PendingException();
191
    }
192
193
    /**
194
     * @Then /^target "([^"]*)" has exactly (\d+) sale for customer$/
195
     */
196
    public function targetHasExactlyNSaleForCustomer(string $target, string $count)
197
    {
198
        // TODO: implement
199
        // $sales = $this->builder->findSales(['target-name' => $target]);
200
201
        Assert::assertCount($count, $sales);
202
    }
203
204
    /**
205
     * @Given /purchase target (\S+) by plan (\S+) at ([-:\w\s]+)$/
206
     */
207
    public function purchaseTarget(string $target, string $plan, string $time): void
208
    {
209
        $time = $this->prepareTime($time);
210
        $this->builder->buildPurchase($target, $plan, $time);
211
    }
212
213
    /**
214
     * @Given /^purchase target "([^"]*)" by plan "([^"]*)" at "([^"]*)" with the following initial uses:$/
215
     */
216
    public function purchaseTargetWithInitialUses(string $target, string $plan, string $time, TableNode $usesTable): void
217
    {
218
        $time = $this->prepareTime($time);
219
        $uses = array_map(static function (array $row) {
220
            return [
221
                'type' => $row['type'],
222
                'unit' => $row['unit'],
223
                'amount' => $row['amount'],
224
            ];
225
        }, $usesTable->getColumnsHash());
226
227
        $this->mayFail(
228
            fn() => $this->builder->buildPurchase($target, $plan, $time, $uses)
229
        );
230
    }
231
232
    /**
233
     * @Given /resource consumption for (\S+) is +(\S+) (\S+) for target (\S+) at (.+)$/
234
     */
235
    public function setConsumption(string $type, int $amount, string $unit, string $target, string $time): void
236
    {
237
        $time = $this->prepareTime($time);
238
        $this->builder->setConsumption($type, $amount, $unit, $target, $time);
239
    }
240
241
    /**
242
     * @Given /recalculate autotariff for target (\S+)( +at (\S+))?$/
243
     */
244
    public function recalculateAutoTariff(string $target, string $time = null): void
245
    {
246
        $this->builder->clientSetAutoTariff($target, $time);
247
    }
248
249
    /**
250
     * @Given /perform billing at (\S+)/
251
     */
252
    public function performBilling(string $time): void
253
    {
254
        $this->builder->performBilling($this->prepareTime($time));
255
    }
256
257
    /**
258
     * @Given /action for (\S+) is +(\S+) (\S+) +for target (.+?)( +at (\S+))?$/
259
     */
260
    public function setAction(string $type, int $amount, string $unit, string $target, string $at = null, string $time = null): void
261
    {
262
        $time = $this->prepareTime($time);
263
        $this->builder->setAction($type, $amount, $unit, $target, $time);
264
    }
265
266
    /**
267
     * @Given /perform calculation( at (\S+))?/
268
     */
269
    public function performCalculation(string $at = null, string $time = null): array
270
    {
271
        $this->charges = $this->builder->performCalculation($this->prepareTime($time));
272
        return $this->charges;
273
    }
274
275
    /**
276
     * @Given /bill +for (\S+) is +(\S+) (\S+) per (\S+) (\S+) for target (.+?)( +at (.+))?$/
277
     */
278
    public function billWithTime($type, $sum, $currency, $quantity, $unit, $target, $at = null, $time = null)
279
    {
280
        $this->builder->flushEntitiesCacheByType('bill');
281
282
        $quantity = $this->prepareQuantity($quantity);
283
        $sum = $this->prepareSum($sum, $quantity);
284
        $time = $this->prepareTime($time);
285
        $bill = $this->findBill([
286
            'type' => $type,
287
            'target' => $target,
288
            'sum' => "$sum $currency",
289
            'quantity' => "$quantity $unit",
290
            'time' => $time,
291
        ]);
292
        Assert::assertSame($type, $bill->getType()->getName(), "Bill type mismatch: expected $type, got {$bill->getType()->getName()}");
293
        Assert::assertSame($target, $bill->getTarget()->getFullName(), "Bill target mismatch: expected $target, got {$bill->getTarget()->getFullName()}");
294
        Assert::assertEquals(bcmul($sum, 100), $bill->getSum()->getAmount(), "Bill sum mismatch: expected $sum, got {$bill->getSum()->getAmount()}");
295
        Assert::assertSame($currency, $bill->getSum()->getCurrency()->getCode(), "Bill currency mismatch: expected $currency, got {$bill->getSum()->getCurrency()->getCode()}");
296
        Assert::assertEquals((float)$quantity, (float)$bill->getQuantity()->getQuantity(), "Bill quantity mismatch: expected $quantity, got {$bill->getQuantity()->getQuantity()}");
297
        Assert::assertEquals(strtolower($unit), strtolower($bill->getQuantity()->getUnit()->getName()), "Bill unit mismatch: expected $unit, got {$bill->getQuantity()->getUnit()->getName()}");
298
        if ($time) {
299
            Assert::assertEquals(new DateTimeImmutable($time), $bill->getTime(), "Bill time mismatch: expected $time, got {$bill->getTime()->format(DATE_ATOM)}");
300
        }
301
    }
302
303
    /**
304
     * @Given /bill interval +for (\S+) is +(\S+) (\S+) per (\S+) (\S+) for target (.+?) at (\S+) between (\S+) and (\S+)?$/
305
     */
306
    public function billInterval($type, $sum, $currency, $quantity, $unit, $target, $time, $since, $till)
307
    {
308
        $this->builder->flushEntitiesCacheByType('bill');
309
310
        $quantity = $this->prepareQuantity($quantity);
311
        $sum = $this->prepareSum($sum, $quantity);
312
        $time = $this->prepareTime($time);
313
        $bill = $this->findBill([
314
            'type' => $type,
315
            'target' => $target,
316
            'sum' => "$sum $currency",
317
            'quantity' => "$quantity $unit",
318
            'time' => $time,
319
        ]);
320
        Assert::assertSame($type, $bill->getType()->getName(), "Bill type mismatch: expected $type, got {$bill->getType()->getName()}");
321
        Assert::assertSame($target, $bill->getTarget()->getFullName(), "Bill target mismatch: expected $target, got {$bill->getTarget()->getFullName()}");
322
        Assert::assertEquals(bcmul($sum, 100), $bill->getSum()->getAmount(), "Bill sum mismatch: expected $sum, got {$bill->getSum()->getAmount()}");
323
        Assert::assertSame($currency, $bill->getSum()->getCurrency()->getCode(), "Bill currency mismatch: expected $currency, got {$bill->getSum()->getCurrency()->getCode()}");
324
        Assert::assertEquals((float)$quantity, (float)$bill->getQuantity()->getQuantity(), "Bill quantity mismatch: expected $quantity, got {$bill->getQuantity()->getQuantity()}");
325
        Assert::assertEquals(strtolower($unit), strtolower($bill->getQuantity()->getUnit()->getName()), "Bill unit mismatch: expected $unit, got {$bill->getQuantity()->getUnit()->getName()}");
326
        Assert::assertEquals(new DateTimeImmutable($time), $bill->getTime(), "Bill time mismatch: expected $time, got {$bill->getTime()->format(DATE_ATOM)}");
327
        $billStart = $bill->getUsageInterval()->start();
328
        $billEnd = $bill->getUsageInterval()->end();
329
        Assert::assertEquals(new DateTimeImmutable($since), $billStart, "Bill since time mismatch: expected $since, got {$billStart->format(DATE_ATOM)}");
330
        Assert::assertEquals(new DateTimeImmutable($till), $billEnd, "Bill till time mismatch: expected $till, got {$billEnd->format(DATE_ATOM)}");
331
    }
332
333
    public function findBill(array $params): BillInterface
334
    {
335
        $bills = $this->builder->findBills($params);
336
        $this->bill = reset($bills);
337
        $this->charges = $this->bill->getCharges();
338
339
        return $this->bill;
340
    }
341
342
    /**
343
     * @Given /bills number is (\d+) for (\S+) for target (.+?)( +at (\S+))?$/
344
     */
345
    public function billsNumberWithTime($number, $type, $target, $at = null, $time = null)
346
    {
347
        $count = count($this->builder->findBills(array_filter([
348
            'type' => $type,
349
            'target' => $target,
350
            'time' => $this->prepareTime($time),
351
        ])));
352
353
        Assert::assertEquals($number, $count);
354
    }
355
356
    /**
357
     * @Given /charges number is (\d+)/
358
     */
359
    public function chargesNumber($number)
360
    {
361
        Assert::assertEquals($number, count($this->charges));
362
    }
363
364
    /**
365
     * @Given /charge for (\S+) is +(\S+) (\S+) per (\S+) (\S+)$/
366
     */
367
    public function charge($type, $amount, $currency, $quantity, $unit)
368
    {
369
        $this->chargeWithTarget($type, $amount, $currency, $quantity, $unit, null);
370
    }
371
372
    /**
373
     * @Given /charge for (\S+) is +(\S+) (\S+) per +(\S+) (\S+) +for target (.+?)( +at (\S+))?$/
374
     */
375
    public function chargeWithTarget($type, $amount, $currency, $quantity, $unit, $target, $at = null, $time = null)
376
    {
377
        $quantity = $this->prepareQuantity($quantity);
378
        $amount = $this->prepareSum($amount, $quantity);
379
        $charge = $this->findCharge($type, $target);
380
        Assert::assertNotNull($charge);
381
        Assert::assertSame($type, $charge->getType()->getName());
382
        Assert::assertSame($target, $charge->getTarget()->getFullName());
383
        Assert::assertEquals(bcmul($amount, 100), (int)$charge->getSum()->getAmount());
384
        Assert::assertSame($currency, $charge->getSum()->getCurrency()->getCode());
385
        Assert::assertEquals((float)$quantity, (float)$charge->getUsage()->getQuantity());
386
        Assert::assertEquals(strtolower($unit), strtolower($charge->getUsage()->getUnit()->getName()));
387
    }
388
389
    public function findCharge($type, $target): ?ChargeInterface
390
    {
391
        foreach ($this->charges as $charge) {
392
            if ($charge->getType()->getName() !== $type) {
393
                continue;
394
            }
395
            if ($charge->getTarget()->getFullName() !== $target) {
396
                continue;
397
            }
398
399
            return $charge;
400
        }
401
402
        return null;
403
    }
404
405
    public function getNextCharge(): ChargeInterface
406
    {
407
        $charge = current($this->charges);
408
        next($this->charges);
409
410
        return $charge;
411
    }
412
413
    /**
414
     * @return string|false|null
415
     */
416
    protected function prepareTime(string $time = null)
417
    {
418
        if ($time === null) {
419
            return null;
420
        }
421
422
        if ($time === 'midnight second day of this month') {
423
            return date('Y-m-02');
424
        }
425
        if (strncmp($time, 'pY', 1) === 0) {
426
            return date(substr($time, 1), strtotime('-1 year'));
427
        }
428
        if (str_contains($time, 'nm')) {
429
            $format = str_replace('nm', 'm', $time);
430
            return date($format, strtotime('next month'));
431
        }
432
        if (str_contains($time, 'pm')) {
433
            $time = str_replace('pm', 'm', $time);
434
            $time = date($time, strtotime('-1 month'));
435
        }
436
        if (strncmp($time, 'Y', 1) === 0) {
437
            return date($time);
438
        }
439
440
        return $time;
441
    }
442
443
    private function prepareQuantity($quantity)
444
    {
445
        if ($quantity[0] === 's') {
446
            return $this->getSaleQuantity();
447
        }
448
449
        return $quantity;
450
    }
451
452
    private function prepareSum($sum, $quantity)
453
    {
454
        if ($sum[0] === 's') {
455
            $sum = round(substr($sum, 1) * $quantity*100)/100;
456
        }
457
458
        return $sum;
459
    }
460
461
    public function getSaleQuantity()
462
    {
463
        return $this->days2quantity(new DateTimeImmutable($this->saleTime));
464
    }
465
466
    private function days2quantity(DateTimeImmutable $from)
467
    {
468
        $till = new DateTimeImmutable('first day of next month midnight');
469
        $diff = $from->diff($till);
470
        if ($diff->m) {
471
            return 1;
472
        }
473
474
        return $diff->d/date('t');
475
    }
476
477
    /**
478
     * @When /^tariff plan change is requested for target "([^"]*)" to plan "([^"]*)" at "([^"]*)"$/
479
     */
480
    public function tariffPlanChangeIsRequestedForTarget(string $target, string $planName, string $date)
481
    {
482
        $this->mayFail(fn () => $this->builder->targetChangePlan($target, $planName, $this->prepareTime($date)));
483
    }
484
485
    /**
486
     * @When /^tariff plan change is requested for target "([^"]*)" to plan "([^"]*)" at "([^"]*)", assuming current time is "([^"]*)"$/
487
     */
488
    public function tariffPlanChangeIsRequestedForTargetAtSpecificTime(string $target, string $planName, string $date, ?string $wallTime = null)
489
    {
490
        $this->mayFail(fn () => $this->builder->targetChangePlan($target, $planName, $this->prepareTime($date), $this->prepareTime($wallTime)));
491
    }
492
493
    /**
494
     * @Then /^target "([^"]*)" is sold to customer by plan "([^"]*)" since "([^"]*)"(?: till "([^"]*)")?$/
495
     */
496
    public function targetIsSoldToCustomerByPlanSinceTill(string $target, string $planName, string $saleDate, ?string $saleCloseDate = null)
497
    {
498
        $sales = $this->builder->findHistoricalSales([
499
            'target' => $target,
500
        ]);
501
502
        $saleDateTime = new DateTimeImmutable('@' . strtotime($this->prepareTime($saleDate)));
503
        $saleCloseDateTime = $saleCloseDate ? new DateTimeImmutable('@' . strtotime($this->prepareTime($saleCloseDate))) : null;
504
505
        foreach ($sales as $sale) {
506
            /** @noinspection PhpBooleanCanBeSimplifiedInspection */
507
            $saleExists = true
508
                && str_contains($sale->getPlan()->getName(), $planName)
509
                && $sale->getTime()->format(DATE_ATOM) === $saleDateTime->format(DATE_ATOM)
510
                && (
511
                    ($saleCloseDate === null && $sale->getCloseTime() === null)
512
                    ||
513
                    ($saleCloseDate !== null && $sale->getCloseTime()->format(DATE_ATOM) === $saleCloseDateTime->format(DATE_ATOM))
514
                );
515
516
            if ($saleExists) {
517
                return;
518
            }
519
        }
520
521
        Assert::fail('Requested sale does not exist');
522
    }
523
524
    /**
525
     * @Then /^target "([^"]*)" has exactly (\d+) sales for customer$/
526
     */
527
    public function targetHasExactlySalesForCustomer(string $target, int $count)
528
    {
529
        $sales = $this->builder->findHistoricalSales([
530
            'target' => $target,
531
        ]);
532
533
        Assert::assertCount($count, $sales);
534
    }
535
536
    /**
537
     * @Then /^caught error is "([^"]*)"$/
538
     */
539
    public function caughtErrorIs(string $errorMessage): void
540
    {
541
        $this->assertCaughtExceptionMatches(\Throwable::class, $errorMessage);
542
    }
543
544
    /**
545
     * @Given /^target "([^"]*)"$/
546
     */
547
    public function target(string $target)
548
    {
549
        $this->builder->buildTarget($target);
550
    }
551
552
    /**
553
     * @Then /^flush entities cache$/
554
     */
555
    public function flushEntitiesCache()
556
    {
557
        $this->builder->flushEntitiesCache();
558
    }
559
560
    /**
561
     * @Given /^target "([^"]*)" has the following uses:$/
562
     */
563
    public function targetHasTheFollowingUses(string $target, TableNode $usesTable)
564
    {
565
        foreach ($usesTable->getColumnsHash() as $row) {
566
            $uses = $this->builder->findUsage($row['time'], $target, $row['type']);
567
            Assert::assertCount(1, $uses);
568
569
            $use = reset($uses);
570
            Assert::assertSame(
571
                $row['unit'], $use['unit'],
572
                sprintf('Exptected unit to be %s, got %s instead', $row['unit'], $use['unit'])
573
            );
574
            Assert::assertEquals(
575
                $row['amount'], $use['total'],
576
                sprintf('Exptected total to be %s, got %s instead', $row['amount'], $use['total'])
577
            );
578
        }
579
    }
580
}
581