Completed
Push — master ( 881b3b...090ef2 )
by Michał
10:07
created

CompletePage::hasShippingPromotion()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 4
rs 10
c 0
b 0
f 0
cc 1
eloc 2
nc 1
nop 1
1
<?php
2
3
/*
4
 * This file is part of the Sylius package.
5
 *
6
 * (c) Paweł Jędrzejewski
7
 *
8
 * For the full copyright and license information, please view the LICENSE
9
 * file that was distributed with this source code.
10
 */
11
12
namespace Sylius\Behat\Page\Shop\Checkout;
13
14
use Behat\Mink\Element\NodeElement;
15
use Behat\Mink\Session;
16
use Sylius\Behat\Page\SymfonyPage;
17
use Sylius\Behat\Service\Accessor\TableAccessorInterface;
18
use Sylius\Component\Core\Model\AddressInterface;
19
use Sylius\Component\Core\Model\ProductInterface;
20
use Sylius\Component\Core\Model\ShippingMethodInterface;
21
use Sylius\Component\Payment\Model\PaymentMethodInterface;
22
use Symfony\Component\Intl\Intl;
23
use Symfony\Component\Routing\RouterInterface;
24
25
/**
26
 * @author Mateusz Zalewski <[email protected]>
27
 */
28
class CompletePage extends SymfonyPage implements CompletePageInterface
29
{
30
    /**
31
     * @var TableAccessorInterface
32
     */
33
    private $tableAccessor;
34
35
    /**
36
     * @param Session $session
37
     * @param array $parameters
38
     * @param RouterInterface $router
39
     * @param TableAccessorInterface $tableAccessor
40
     */
41
    public function __construct(
42
        Session $session,
43
        array $parameters,
44
        RouterInterface $router,
45
        TableAccessorInterface $tableAccessor
46
    ) {
47
        parent::__construct($session, $parameters, $router);
48
49
        $this->tableAccessor = $tableAccessor;
50
    }
51
52
53
    /**
54
     * {@inheritdoc}
55
     */
56
    public function getRouteName()
57
    {
58
        return 'sylius_shop_checkout_complete';
59
    }
60
61
    /**
62
     * {@inheritdoc}
63
     */
64
    public function hasItemWithProductAndQuantity($productName, $quantity)
65
    {
66
        $table = $this->getElement('items_table');
67
68
        try {
69
            $this->tableAccessor->getRowWithFields($table, ['item' => $productName, 'qty' => $quantity]);
70
        } catch (\InvalidArgumentException $exception) {
71
            return false;
72
        }
73
74
        return true;
75
    }
76
77
    /**
78
     * {@inheritdoc}
79
     */
80
    public function hasShippingAddress(AddressInterface $address)
81
    {
82
        $shippingAddress = $this->getElement('shipping_address')->getText();
83
84
        return $this->isAddressValid($shippingAddress, $address);
85
    }
86
87
    /**
88
     * {@inheritdoc}
89
     */
90
    public function hasBillingAddress(AddressInterface $address)
91
    {
92
        $billingAddress = $this->getElement('billing_address')->getText();
93
94
        return $this->isAddressValid($billingAddress, $address);
95
    }
96
97
    /**
98
     * {@inheritdoc}
99
     */
100
    public function hasShippingMethod(ShippingMethodInterface $shippingMethod)
101
    {
102
        if (!$this->hasElement('shipping_method')) {
103
            return false;
104
        }
105
106
        return false !== strpos($this->getElement('shipping_method')->getText(), $shippingMethod->getName());
107
    }
108
109
    /**
110
     * {@inheritdoc}
111
     */
112
    public function getPaymentMethodName()
113
    {
114
        return $this->getElement('payment_method')->getText();
0 ignored issues
show
Bug Best Practice introduced by
The return type of return $this->getElement...nt_method')->getText(); (string) is incompatible with the return type declared by the interface Sylius\Behat\Page\Shop\C...e::getPaymentMethodName of type boolean.

If you return a value from a function or method, it should be a sub-type of the type that is given by the parent type f.e. an interface, or abstract method. This is more formally defined by the Lizkov substitution principle, and guarantees that classes that depend on the parent type can use any instance of a child type interchangably. This principle also belongs to the SOLID principles for object oriented design.

Let’s take a look at an example:

class Author {
    private $name;

    public function __construct($name) {
        $this->name = $name;
    }

    public function getName() {
        return $this->name;
    }
}

abstract class Post {
    public function getAuthor() {
        return 'Johannes';
    }
}

class BlogPost extends Post {
    public function getAuthor() {
        return new Author('Johannes');
    }
}

class ForumPost extends Post { /* ... */ }

function my_function(Post $post) {
    echo strtoupper($post->getAuthor());
}

Our function my_function expects a Post object, and outputs the author of the post. The base class Post returns a simple string and outputting a simple string will work just fine. However, the child class BlogPost which is a sub-type of Post instead decided to return an object, and is therefore violating the SOLID principles. If a BlogPost were passed to my_function, PHP would not complain, but ultimately fail when executing the strtoupper call in its body.

Loading history...
115
    }
116
117
    /**
118
     * {@inheritdoc}
119
     */
120
    public function hasPaymentMethod()
121
    {
122
        return $this->hasElement('payment_method');
123
    }
124
125
    /**
126
     * {@inheritdoc}
127
     */
128
    public function hasProductDiscountedUnitPriceBy(ProductInterface $product, $amount)
129
    {
130
        $columns = $this->getProductRowElement($product)->findAll('css', 'td');
131
        $priceWithoutDiscount = $this->getPriceFromString($columns[1]->getText());
132
        $priceWithDiscount = $this->getPriceFromString($columns[3]->getText());
133
        $discount = $priceWithoutDiscount - $priceWithDiscount;
134
135
        return $discount === $amount;
136
    }
137
138
    /**
139
     * {@inheritdoc}
140
     */
141
    public function hasOrderTotal($total)
142
    {
143
        if (!$this->hasElement('order_total')) {
144
            return false;
145
        }
146
147
        return $this->getTotalFromString($this->getElement('order_total')->getText()) === $total;
148
    }
149
150
    /**
151
     * {@inheritdoc}
152
     */
153
    public function getBaseCurrencyOrderTotal()
154
    {
155
        return $this->getBaseTotalFromString($this->getElement('base_order_total')->getText());
156
    }
157
158
    /**
159
     * {@inheritdoc}
160
     */
161
    public function addNotes($notes)
162
    {
163
        $this->getElement('extra_notes')->setValue($notes);
164
    }
165
166
    /**
167
     * {@inheritdoc}
168
     */
169
    public function hasPromotionTotal($promotionTotal)
170
    {
171
        return false !== strpos($this->getElement('promotion_total')->getText(), $promotionTotal);
172
    }
173
174
    /**
175
     * {@inheritdoc}
176
     */
177
    public function hasPromotion($promotionName)
178
    {
179
        return false !== stripos($this->getElement('promotion_discounts')->getText(), $promotionName);
180
    }
181
182
    /**
183
     * {@inheritdoc}
184
     */
185
    public function hasShippingPromotion($promotionName)
186
    {
187
        return false !== stripos($this->getElement('promotion_shipping_discounts')->getText(), $promotionName);
188
    }
189
190
191
    /**
192
     * {@inheritdoc}
193
     */
194
    public function hasTaxTotal($taxTotal)
195
    {
196
        return false !== strpos($this->getElement('tax_total')->getText(), $taxTotal);
197
    }
198
199
    /**
200
     * {@inheritdoc}
201
     */
202
    public function hasShippingTotal($price)
203
    {
204
        return false !== strpos($this->getElement('shipping_total')->getText(), $price);
205
    }
206
207
    /**
208
     * {@inheritdoc}
209
     */
210
    public function hasProductUnitPrice(ProductInterface $product, $price)
211
    {
212
        $productRowElement = $this->getProductRowElement($product);
213
214
        return null !== $productRowElement->find('css', sprintf('td:contains("%s")', $price));
215
    }
216
217
    /**
218
     * {@inheritdoc}
219
     */
220
    public function hasProductOutOfStockValidationMessage(ProductInterface $product)
221
    {
222
        $message = sprintf('%s does not have sufficient stock.', $product->getName());
223
224
        return $this->getElement('validation_errors')->getText() === $message;
225
    }
226
227
    /**
228
     * {@inheritdoc}
229
     */
230
    public function hasLocale($localeName)
231
    {
232
        return false !== strpos($this->getElement('locale')->getText(), $localeName);
233
    }
234
235
    /**
236
     * {@inheritdoc}
237
     */
238
    public function hasCurrency($currencyCode)
239
    {
240
        return false !== strpos($this->getElement('currency')->getText(), $currencyCode);
241
    }
242
243
    public function confirmOrder()
244
    {
245
        $this->getDocument()->pressButton('Place order');
246
    }
247
248
    public function changeAddress()
249
    {
250
        $this->getElement('addressing_step_label')->click();
251
    }
252
253
    public function changeShippingMethod()
254
    {
255
        $this->getElement('shipping_step_label')->click();
256
    }
257
258
    public function changePaymentMethod()
259
    {
260
        $this->getElement('payment_step_label')->click();
261
    }
262
263
    /**
264
     * {@inheritdoc}
265
     */
266
    public function hasShippingProvinceName($provinceName)
267
    {
268
        $shippingAddressText = $this->getElement('shipping_address')->getText();
269
270
        return false !== stripos($shippingAddressText, $provinceName);
271
    }
272
273
    /**
274
     * {@inheritdoc}
275
     */
276
    public function hasBillingProvinceName($provinceName)
277
    {
278
        $billingAddressText = $this->getElement('billing_address')->getText();
279
280
        return false !== stripos($billingAddressText, $provinceName);
281
    }
282
283
    /**
284
     * {@inheritdoc}
285
     */
286
    public function getShippingPromotionDiscount($promotionName)
287
    {
288
        return $this->getElement('promotion_shipping_discounts')->find('css', '.description')->getText();
289
    }
290
291
    /**
292
     * {@inheritdoc}
293
     */
294
    protected function getDefinedElements()
295
    {
296
        return array_merge(parent::getDefinedElements(), [
297
            'addressing_step_label' => '.steps a:contains("Address")',
298
            'billing_address' => '#sylius-billing-address',
299
            'currency' => '#sylius-order-currency-code',
300
            'extra_notes' =>'#sylius_checkout_complete_notes',
301
            'items_table' => '#sylius-order',
302
            'locale' => '#sylius-order-locale-name',
303
            'order_total' => 'td:contains("Total")',
304
            'base_order_total' => '#base-total',
305
            'payment_method' => '#sylius-payment-method',
306
            'payment_step_label' => '.steps a:contains("Payment")',
307
            'product_row' => 'tbody tr:contains("%name%")',
308
            'promotion_discounts' => '#promotion-discounts',
309
            'promotion_shipping_discounts' => '#promotion-shipping-discounts',
310
            'promotion_total' => '#promotion-total',
311
            'shipping_address' => '#sylius-shipping-address',
312
            'shipping_method' => '#sylius-shipping-method',
313
            'shipping_step_label' => '.steps a:contains("Shipping")',
314
            'shipping_total' => '#shipping-total',
315
            'tax_total' => '#tax-total',
316
            'validation_errors' => '.sylius-validation-error',
317
        ]);
318
    }
319
320
    /**
321
     * @param ProductInterface $product
322
     *
323
     * @return NodeElement
324
     */
325
    private function getProductRowElement(ProductInterface $product)
326
    {
327
        return $this->getElement('product_row', ['%name%' => $product->getName()]);
328
    }
329
330
    /**
331
     * @param string $displayedAddress
332
     * @param AddressInterface $address
333
     *
334
     * @return bool
335
     */
336
    private function isAddressValid($displayedAddress, AddressInterface $address)
337
    {
338
        return
339
            $this->hasAddressPart($displayedAddress, $address->getCompany(), true) &&
340
            $this->hasAddressPart($displayedAddress, $address->getFirstName()) &&
341
            $this->hasAddressPart($displayedAddress, $address->getLastName()) &&
342
            $this->hasAddressPart($displayedAddress, $address->getPhoneNumber(), true) &&
343
            $this->hasAddressPart($displayedAddress, $address->getStreet()) &&
344
            $this->hasAddressPart($displayedAddress, $address->getCity()) &&
345
            $this->hasAddressPart($displayedAddress, $address->getProvinceCode(), true) &&
346
            $this->hasAddressPart($displayedAddress, $this->getCountryName($address->getCountryCode())) &&
347
            $this->hasAddressPart($displayedAddress, $address->getPostcode())
348
        ;
349
    }
350
351
    /**
352
     * @param string $address
353
     * @param string $addressPart
354
     *
355
     * @return bool
356
     */
357
    private function hasAddressPart($address, $addressPart, $optional = false)
358
    {
359
        if ($optional && null === $addressPart) {
360
            return true;
361
        }
362
363
        return false !== strpos($address, $addressPart);
364
    }
365
366
    /**
367
     * @param string $countryCode
368
     *
369
     * @return string
370
     */
371
    private function getCountryName($countryCode)
372
    {
373
        return strtoupper(Intl::getRegionBundle()->getCountryName($countryCode, 'en'));
374
    }
375
376
    /**
377
     * @param string $price
378
     *
379
     * @return int
380
     */
381
    private function getPriceFromString($price)
382
    {
383
        return (int) round(str_replace(['€', '£', '$'], '', $price) * 100, 2);
384
    }
385
386
    /**
387
     * @param string $total
388
     *
389
     * @return int
390
     */
391
    private function getTotalFromString($total)
392
    {
393
        $total = str_replace('Total:', '', $total);
394
395
        return $this->getPriceFromString($total);
396
    }
397
398
    /**
399
     * @param string $total
400
     *
401
     * @return int
402
     */
403
    private function getBaseTotalFromString($total)
404
    {
405
        $total = str_replace('Total in base currency:', '', $total);
406
407
        return $this->getPriceFromString($total);
408
    }
409
}
410