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 Closure; |
15
|
|
|
use DateTimeImmutable; |
16
|
|
|
use hiqdev\php\billing\action\Action; |
17
|
|
|
use hiqdev\php\billing\charge\Charge; |
18
|
|
|
use hiqdev\php\billing\charge\ChargeInterface; |
19
|
|
|
use hiqdev\php\billing\customer\Customer; |
20
|
|
|
use hiqdev\php\billing\formula\FormulaEngine; |
21
|
|
|
use hiqdev\php\billing\price\SinglePrice; |
22
|
|
|
use hiqdev\php\billing\target\Target; |
23
|
|
|
use hiqdev\php\billing\type\Type; |
24
|
|
|
use hiqdev\php\units\Quantity; |
25
|
|
|
use Money\Currencies\ISOCurrencies; |
26
|
|
|
use Money\Parser\DecimalMoneyParser; |
27
|
|
|
use NumberFormatter; |
28
|
|
|
use PHPUnit\Framework\Assert; |
29
|
|
|
|
30
|
|
|
/** |
31
|
|
|
* Defines application features from the specific context. |
32
|
|
|
*/ |
33
|
|
|
class FeatureContext implements Context |
34
|
|
|
{ |
35
|
|
|
protected $engine; |
36
|
|
|
|
37
|
|
|
/** @var Customer */ |
38
|
|
|
protected $customer; |
39
|
|
|
/** |
40
|
|
|
* @var \hiqdev\php\billing\price\PriceInterface|\hiqdev\php\billing\charge\FormulaChargeModifierTrait |
41
|
|
|
* |
42
|
|
|
* TODO: FormulaChargeModifierTrait::setFormula() must be moved to interface |
43
|
|
|
*/ |
44
|
|
|
protected $price; |
45
|
|
|
|
46
|
|
|
/** @var string */ |
47
|
|
|
protected $formula; |
48
|
|
|
|
49
|
|
|
/** |
50
|
|
|
* @var \hiqdev\php\billing\action\ActionInterface|\hiqdev\php\billing\action\AbstractAction |
51
|
|
|
*/ |
52
|
|
|
protected $action; |
53
|
|
|
/** |
54
|
|
|
* @var ChargeInterface[] |
55
|
|
|
*/ |
56
|
|
|
protected $charges; |
57
|
|
|
|
58
|
|
|
/** @var \Money\MoneyParser */ |
59
|
|
|
protected $moneyParser; |
60
|
|
|
|
61
|
|
|
/** @var string */ |
62
|
|
|
protected $expectedError; |
63
|
|
|
|
64
|
|
|
/** |
65
|
|
|
* Initializes context. |
66
|
|
|
*/ |
67
|
|
|
public function __construct() |
68
|
|
|
{ |
69
|
|
|
$this->customer = new Customer(null, 'somebody'); |
70
|
|
|
$this->moneyParser = new DecimalMoneyParser(new ISOCurrencies()); |
71
|
|
|
} |
72
|
|
|
|
73
|
|
|
/** |
74
|
|
|
* @Given /(\S+) (\S+) price is ([0-9.]+) (\w+) per (\w+)/ |
75
|
|
|
*/ |
76
|
|
View Code Duplication |
public function priceIs($target, $type, $sum, $currency, $unit) |
|
|
|
|
77
|
|
|
{ |
78
|
|
|
$type = new Type(Type::ANY, $type); |
79
|
|
|
$target = new Target(Target::ANY, $target); |
80
|
|
|
$quantity = Quantity::create($unit, 0); |
81
|
|
|
$sum = $this->moneyParser->parse($sum, $currency); |
82
|
|
|
$this->price = new SinglePrice(null, $type, $target, null, $quantity, $sum); |
83
|
|
|
} |
84
|
|
|
|
85
|
|
|
/** |
86
|
|
|
* @Given /action is (\S+) (\w+) ([0-9.]+) (\S+)/ |
87
|
|
|
*/ |
88
|
|
View Code Duplication |
public function actionIs($target, $type, $amount, $unit) |
|
|
|
|
89
|
|
|
{ |
90
|
|
|
$type = new Type(Type::ANY, $type); |
91
|
|
|
$target = new Target(Target::ANY, $target); |
92
|
|
|
$quantity = Quantity::create($unit, $amount); |
93
|
|
|
$time = new DateTimeImmutable(); |
94
|
|
|
$this->action = new Action(null, $type, $target, $quantity, $this->customer, $time); |
95
|
|
|
} |
96
|
|
|
|
97
|
|
|
/** |
98
|
|
|
* @Given /formula is (.+)/ |
99
|
|
|
* @param string $formula |
100
|
|
|
*/ |
101
|
|
|
public function formulaIs(string $formula): void |
102
|
|
|
{ |
103
|
|
|
$this->formula = $formula; |
104
|
|
|
} |
105
|
|
|
|
106
|
|
|
/** |
107
|
|
|
* @Given /formula continues (.+)/ |
108
|
|
|
* @param string $formula |
109
|
|
|
*/ |
110
|
|
|
public function formulaContinues(string $formula): void |
111
|
|
|
{ |
112
|
|
|
$this->formula .= "\n" . $formula; |
113
|
|
|
} |
114
|
|
|
|
115
|
|
|
protected function getFormulaEngine() |
116
|
|
|
{ |
117
|
|
|
if ($this->engine === null) { |
118
|
|
|
$this->engine = new FormulaEngine(); |
119
|
|
|
} |
120
|
|
|
|
121
|
|
|
return $this->engine; |
122
|
|
|
} |
123
|
|
|
|
124
|
|
|
/** |
125
|
|
|
* @When /action date is ([0-9.-]+)/ |
126
|
|
|
* @param string $date |
127
|
|
|
* @throws \Exception |
128
|
|
|
*/ |
129
|
|
|
public function actionDateIs(string $date): void |
130
|
|
|
{ |
131
|
|
|
$this->action->setTime(new DateTimeImmutable($date)); |
|
|
|
|
132
|
|
|
} |
133
|
|
|
|
134
|
|
|
/** |
135
|
|
|
* @Then /^error is$/m |
136
|
|
|
*/ |
137
|
|
|
public function multilineErrorIs(\Behat\Gherkin\Node\PyStringNode $value) |
138
|
|
|
{ |
139
|
|
|
$this->expectedError = $value->getRaw(); |
140
|
|
|
} |
141
|
|
|
|
142
|
|
|
/** |
143
|
|
|
* @Then /^error is (.+)$/ |
144
|
|
|
* |
145
|
|
|
* @param string $error |
146
|
|
|
*/ |
147
|
|
|
public function errorIs($error): void |
148
|
|
|
{ |
149
|
|
|
$this->expectedError = $error; |
150
|
|
|
} |
151
|
|
|
|
152
|
|
|
/** |
153
|
|
|
* @Then /^(\w+) charge is ?$/ |
154
|
|
|
* @param string $numeral |
155
|
|
|
*/ |
156
|
|
|
public function emptyCharge(string $numeral): void |
157
|
|
|
{ |
158
|
|
|
$this->chargeIs($numeral); |
159
|
|
|
} |
160
|
|
|
|
161
|
|
|
/** |
162
|
|
|
* @Then /^(\w+) charge is (\S+) +([0-9.]+) ([A-Z]{3})$/ |
163
|
|
|
*/ |
164
|
|
|
public function chargeWithSum($numeral, $type = null, $sum = null, $currency = null): void |
165
|
|
|
{ |
166
|
|
|
$this->chargeIs($numeral, $type, $sum, $currency); |
167
|
|
|
} |
168
|
|
|
|
169
|
|
|
/** |
170
|
|
|
* @Then /^(\w+) charge is (\S+) +([0-9.]+) ([A-Z]{3}) reason (.+)/ |
171
|
|
|
*/ |
172
|
|
|
public function chargeWithReason($numeral, $type = null, $sum = null, $currency = null, $reason = null): void |
173
|
|
|
{ |
174
|
|
|
$this->chargeIs($numeral, $type, $sum, $currency, $reason); |
175
|
|
|
} |
176
|
|
|
|
177
|
|
|
public function chargeIs($numeral, $type = null, $sum = null, $currency = null, $reason = null): void |
178
|
|
|
{ |
179
|
|
|
$no = $this->ensureNo($numeral); |
180
|
|
|
if ($no === 0) { |
181
|
|
|
$this->calculateCharges(); |
182
|
|
|
} |
183
|
|
|
$this->assertCharge($this->charges[$no] ?? null, $type, $sum, $currency, $reason); |
184
|
|
|
} |
185
|
|
|
|
186
|
|
|
/** |
187
|
|
|
* @When /^calculating charges$/ |
188
|
|
|
*/ |
189
|
|
|
public function calculateCharges(): void |
190
|
|
|
{ |
191
|
|
|
$this->expectError(function () { |
192
|
|
|
$this->price->setFormula($this->getFormulaEngine()->build($this->formula)); |
|
|
|
|
193
|
|
|
$this->charges = $this->price->calculateCharges($this->action); |
|
|
|
|
194
|
|
|
}); |
195
|
|
|
} |
196
|
|
|
|
197
|
|
|
public function expectError(Closure $closure): void |
198
|
|
|
{ |
199
|
|
|
try { |
200
|
|
|
call_user_func($closure); |
201
|
|
|
} catch (\Exception $e) { |
202
|
|
|
if ($this->isExpectedError($e)) { |
203
|
|
|
$this->expectedError = null; |
204
|
|
|
} else { |
205
|
|
|
throw $e; |
206
|
|
|
} |
207
|
|
|
} |
208
|
|
|
if ($this->expectedError) { |
|
|
|
|
209
|
|
|
throw new \Exception('failed receive expected exception'); |
210
|
|
|
} |
211
|
|
|
} |
212
|
|
|
|
213
|
|
|
protected function isExpectedError(\Exception $e): bool |
214
|
|
|
{ |
215
|
|
|
return $this->startsWith($e->getMessage(), $this->expectedError); |
216
|
|
|
} |
217
|
|
|
|
218
|
|
|
protected function startsWith(string $string, string $prefix = null): bool |
219
|
|
|
{ |
220
|
|
|
return $prefix && strncmp($string, $prefix, strlen($prefix)) === 0; |
|
|
|
|
221
|
|
|
} |
222
|
|
|
|
223
|
|
|
/** |
224
|
|
|
* @param ChargeInterface|null $charge |
225
|
|
|
* @param string|null $type |
226
|
|
|
* @param string|null $sum |
227
|
|
|
* @param string|null $currency |
228
|
|
|
* @param string|null $reason |
229
|
|
|
*/ |
230
|
|
|
public function assertCharge($charge, $type, $sum, $currency, $reason): void |
231
|
|
|
{ |
232
|
|
|
if (empty($type) && empty($sum) && empty($currency)) { |
233
|
|
|
Assert::assertNull($charge); |
234
|
|
|
|
235
|
|
|
return; |
236
|
|
|
} |
237
|
|
|
Assert::assertInstanceOf(Charge::class, $charge); |
238
|
|
|
Assert::assertSame($type, $charge->getPrice()->getType()->getName()); |
|
|
|
|
239
|
|
|
$money = $this->moneyParser->parse($sum, $currency); |
240
|
|
|
Assert::assertEquals($money, $charge->getSum()); // TODO: Should we add `getSum()` to ChargeInterface? |
241
|
|
|
if ($reason !== null) { |
242
|
|
|
Assert::assertSame($reason, $charge->getComment()); // TODO: Should we add `getComment()` to ChargeInterface? |
243
|
|
|
} |
244
|
|
|
} |
245
|
|
|
|
246
|
|
|
private function ensureNo(string $numeral): int |
247
|
|
|
{ |
248
|
|
|
$formatter = new NumberFormatter('en_EN', NumberFormatter::SPELLOUT); |
249
|
|
|
$result = $formatter->parse($numeral); |
250
|
|
|
if ($result === false) { |
251
|
|
|
throw new \Exception("Wrong numeral '$numeral'"); |
252
|
|
|
} |
253
|
|
|
|
254
|
|
|
return --$result; |
255
|
|
|
} |
256
|
|
|
} |
257
|
|
|
|
Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.
You can also find more detailed suggestions in the “Code” section of your repository.