Completed
Push — sf/improvement-coverage ( b3937e...01a837 )
by Kiyotaka
51:20 queued 45:08
created

PointProcessor::validate()   A

Complexity

Conditions 4
Paths 5

Size

Total Lines 22

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 11
CRAP Score 4

Importance

Changes 0
Metric Value
cc 4
nc 5
nop 2
dl 0
loc 22
rs 9.568
c 0
b 0
f 0
ccs 11
cts 11
cp 1
crap 4
1
<?php
2
3
/*
4
 * This file is part of EC-CUBE
5
 *
6
 * Copyright(c) LOCKON CO.,LTD. All Rights Reserved.
7
 *
8
 * http://www.lockon.co.jp/
9
 *
10
 * For the full copyright and license information, please view the LICENSE
11
 * file that was distributed with this source code.
12
 */
13
14
namespace Eccube\Service\PurchaseFlow\Processor;
15
16
use Doctrine\ORM\EntityManagerInterface;
17
use Eccube\Entity\BaseInfo;
18
use Eccube\Entity\ItemHolderInterface;
19
use Eccube\Entity\ItemInterface;
20
use Eccube\Entity\Master\OrderItemType;
21
use Eccube\Entity\Master\TaxDisplayType;
22
use Eccube\Entity\Master\TaxType;
23
use Eccube\Entity\Order;
24
use Eccube\Entity\OrderItem;
25
use Eccube\Repository\BaseInfoRepository;
26
use Eccube\Service\PurchaseFlow\ItemHolderPreprocessor;
27
use Eccube\Service\PurchaseFlow\ItemHolderValidator;
28
use Eccube\Service\PurchaseFlow\PurchaseContext;
29
use Eccube\Service\PurchaseFlow\PurchaseProcessor;
30
31
/**
32
 * 購入フローにおけるポイント処理.
33
 */
34
class PointProcessor extends ItemHolderValidator implements ItemHolderPreprocessor, PurchaseProcessor
35
{
36
    /**
37
     * @var EntityManagerInterface
38
     */
39
    protected $entityManager;
40
41
    /**
42
     * @var BaseInfo
43
     */
44
    protected $BaseInfo;
45
46
    /**
47
     * AddPointProcessor constructor.
48
     *
49
     * @param EntityManagerInterface $entityManager
50
     * @param BaseInfoRepository $baseInfoRepository
51
     */
52 789
    public function __construct(EntityManagerInterface $entityManager, BaseInfoRepository $baseInfoRepository)
53
    {
54 789
        $this->entityManager = $entityManager;
55 789
        $this->BaseInfo = $baseInfoRepository->get();
56
    }
57
58
    /*
59
     * ItemHolderPreprocessor
60
     */
61
62
    /**
63
     * {@inheritdoc}
64
     */
65 232
    public function process(ItemHolderInterface $itemHolder, PurchaseContext $context)
66
    {
67 232
        if (!$this->supports($itemHolder)) {
68 17
            return;
69
        }
70
71 215
        $this->removePointDiscountItem($itemHolder);
72
73
        // 利用ポイントがある場合は割引明細を追加
74 215
        if ($itemHolder->getUsePoint() > 0) {
75 17
            $discount = $this->pointToPrice($itemHolder->getUsePoint());
76 17
            $this->addPointDiscountItem($itemHolder, $discount);
77
        }
78
79
        // 付与ポイントを計算
80 215
        $addPoint = $this->calculateAddPoint($itemHolder);
81 215
        $itemHolder->setAddPoint($addPoint);
82
    }
83
84
    /*
85
     * ItemHolderValidator
86
     */
87
88
    /**
89
     * {@inheritdoc}
90
     */
91 61
    protected function validate(ItemHolderInterface $itemHolder, PurchaseContext $context)
92
    {
93 61
        if (!$this->supports($itemHolder)) {
94 17
            return;
95
        }
96
97
        // 所有ポイント < 利用ポイント
98 44
        $Customer = $itemHolder->getCustomer();
99 44
        if ($Customer->getPoint() < $itemHolder->getUsePoint()) {
100
            // 利用ポイントが所有ポイントを上回っていた場合は所有ポイントで上書き
101 2
            $itemHolder->setUsePoint($Customer->getPoint());
102 2
            $this->throwInvalidItemException('利用ポイントが所有ポイントを上回っています.');
103
        }
104
105
        // 支払い金額 < 利用ポイント
106 42
        if ($itemHolder->getTotal() < 0) {
107
            // 利用ポイントが支払い金額を上回っていた場合は支払い金額が0円以上となるようにポイントを調整
108 1
            $overPoint = floor($itemHolder->getTotal() / $this->BaseInfo->getPointConversionRate());
109 1
            $itemHolder->setUsePoint($itemHolder->getUsePoint() + $overPoint);
110 1
            $this->throwInvalidItemException('利用ポイントがお支払い金額を上回っています.');
111
        }
112
    }
113
114
    /*
115
     * PurchaseProcessor
116
     */
117
118
    /**
119
     * {@inheritdoc}
120
     */
121 12 View Code Duplication
    public function prepare(ItemHolderInterface $itemHolder, PurchaseContext $context)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

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.

Loading history...
122
    {
123 12
        if (!$this->supports($itemHolder)) {
124 3
            return;
125
        }
126
127
        // ユーザの保有ポイントを減算
128 9
        $Customer = $itemHolder->getCustomer();
129 9
        $Customer->setPoint($Customer->getPoint() - $itemHolder->getUsePoint());
130
    }
131
132
    /**
133
     * {@inheritdoc
134
     */
135
    public function commit(ItemHolderInterface $target, PurchaseContext $context)
136
    {
137
        // 何もしない
138
    }
139
140
    /**
141
     * {@inheritdoc
142
     */
143 2 View Code Duplication
    public function rollback(ItemHolderInterface $itemHolder, PurchaseContext $context)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

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.

Loading history...
144
    {
145
        // 利用したポイントをユーザに戻す.
146 2
        if (!$this->supports($itemHolder)) {
147
            return;
148
        }
149
150 2
        $Customer = $itemHolder->getCustomer();
151 2
        $Customer->setPoint($Customer->getPoint() + $itemHolder->getUsePoint());
152
    }
153
154
    /*
155
     * Helper methods
156
     */
157
158
    /**
159
     * Processorが実行出来るかどうかを返す.
160
     *
161
     * 以下を満たす場合に実行できる.
162
     *
163
     * - ポイント設定が有効であること.
164
     * - $itemHolderがOrderエンティティであること.
165
     * - 会員のOrderであること.
166
     *
167
     * @param ItemHolderInterface $itemHolder
168
     *
169
     * @return bool
170
     */
171 239
    private function supports(ItemHolderInterface $itemHolder)
172
    {
173 239
        if (!$this->BaseInfo->isOptionPoint()) {
174
            return false;
175
        }
176
177 239
        if (!$itemHolder instanceof Order) {
178
            return false;
179
        }
180
181 239
        if (!$itemHolder->getCustomer()) {
182 17
            return false;
183
        }
184
185 222
        return true;
186
    }
187
188
    /**
189
     * 付与ポイントを計算.
190
     *
191
     * @param ItemHolderInterface $itemHolder
192
     *
193
     * @return int
194
     */
195 215
    private function calculateAddPoint(ItemHolderInterface $itemHolder)
196
    {
197 215
        $basicPointRate = $this->BaseInfo->getBasicPointRate();
198
199
        // 明細ごとのポイントを集計
200 215
        $totalPoint = array_reduce($itemHolder->getItems()->toArray(), function ($carry, ItemInterface $item) use ($basicPointRate) {
0 ignored issues
show
Bug introduced by
The method toArray cannot be called on $itemHolder->getItems() (of type array<integer,object<Ecc...\Entity\ItemInterface>>).

Methods can only be called on objects. This check looks for methods being called on variables that have been inferred to never be objects.

Loading history...
201 215
            $pointRate = $item->getPointRate();
202 215
            if ($pointRate === null) {
203 215
                $pointRate = $basicPointRate;
204
            }
205
206
            // TODO: ポイントは税抜き分しか割引されない、ポイント明細は税抜きのままでいいのか?
207 215
            if ($item->isPoint()) {
208 17
                $point = round($item->getPrice() * ($pointRate / 100)) * $item->getQuantity();
209
            } else {
210
                // ポイント = 単価 * ポイント付与率 * 数量
211 214
                $point = round($item->getPriceIncTax() * ($pointRate / 100)) * $item->getQuantity();
0 ignored issues
show
Bug introduced by
The method getPriceIncTax() does not exist on Eccube\Entity\ItemInterface. Did you maybe mean getPrice()?

This check marks calls to methods that do not seem to exist on an object.

This is most likely the result of a method being renamed without all references to it being renamed likewise.

Loading history...
212
            }
213
214 215
            return $carry + $point;
215 215
        }, 0);
216
217 215
        return $totalPoint < 0 ? 0 : $totalPoint;
218
    }
219
220
    /**
221
     * 明細追加処理.
222
     *
223
     * @param ItemHolderInterface $itemHolder
224
     * @param integer $discount
225
     */
226 17
    private function addPointDiscountItem(ItemHolderInterface $itemHolder, $discount)
227
    {
228 17
        $DiscountType = $this->entityManager->find(OrderItemType::class, OrderItemType::POINT);
229 17
        $TaxInclude = $this->entityManager->find(TaxDisplayType::class, TaxDisplayType::INCLUDED);
230 17
        $Taxation = $this->entityManager->find(TaxType::class, TaxType::NON_TAXABLE);
231
232
        // TODO TaxProcessorが先行して実行されるため, 税額等の値は個別にセットする.
233 17
        $OrderItem = new OrderItem();
234 17
        $OrderItem->setProductName($DiscountType->getName())
235 17
            ->setPrice($discount)
236 17
            ->setQuantity(1)
237 17
            ->setTax(0)
238 17
            ->setTaxRate(0)
239 17
            ->setTaxRuleId(null)
240 17
            ->setRoundingType(null)
241 17
            ->setOrderItemType($DiscountType)
242 17
            ->setTaxDisplayType($TaxInclude)
243 17
            ->setTaxType($Taxation)
244 17
            ->setOrder($itemHolder);
0 ignored issues
show
Documentation introduced by
$itemHolder is of type object<Eccube\Entity\ItemHolderInterface>, but the function expects a null|object<Eccube\Entity\Order>.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
245 17
        $itemHolder->addItem($OrderItem);
246
    }
247
248
    /**
249
     * 既存のポイント明細を削除する.
250
     *
251
     * @param ItemHolderInterface $itemHolder
252
     */
253 215
    private function removePointDiscountItem(ItemHolderInterface $itemHolder)
254
    {
255 215
        foreach ($itemHolder->getItems() as $item) {
256 214
            if ($item->isPoint()) {
257
                $itemHolder->removeOrderItem($item);
0 ignored issues
show
Bug introduced by
It seems like you code against a concrete implementation and not the interface Eccube\Entity\ItemHolderInterface as the method removeOrderItem() does only exist in the following implementations of said interface: Eccube\Entity\Order.

Let’s take a look at an example:

interface User
{
    /** @return string */
    public function getPassword();
}

class MyUser implements User
{
    public function getPassword()
    {
        // return something
    }

    public function getDisplayName()
    {
        // return some name.
    }
}

class AuthSystem
{
    public function authenticate(User $user)
    {
        $this->logger->info(sprintf('Authenticating %s.', $user->getDisplayName()));
        // do something.
    }
}

In the above example, the authenticate() method works fine as long as you just pass instances of MyUser. However, if you now also want to pass a different implementation of User which does not have a getDisplayName() method, the code will break.

Available Fixes

  1. Change the type-hint for the parameter:

    class AuthSystem
    {
        public function authenticate(MyUser $user) { /* ... */ }
    }
    
  2. Add an additional type-check:

    class AuthSystem
    {
        public function authenticate(User $user)
        {
            if ($user instanceof MyUser) {
                $this->logger->info(/** ... */);
            }
    
            // or alternatively
            if ( ! $user instanceof MyUser) {
                throw new \LogicException(
                    '$user must be an instance of MyUser, '
                   .'other instances are not supported.'
                );
            }
    
        }
    }
    
Note: PHP Analyzer uses reverse abstract interpretation to narrow down the types inside the if block in such a case.
  1. Add the method to the interface:

    interface User
    {
        /** @return string */
        public function getPassword();
    
        /** @return string */
        public function getDisplayName();
    }
    
Loading history...
258 214
                $this->entityManager->remove($item);
259
            }
260
        }
261
    }
262
263
    /**
264
     * ポイントを金額に変換する.
265
     *
266
     * @param integer $point int ポイント
267
     *
268
     * @return int 金額
269
     */
270 17
    private function pointToPrice($point)
271
    {
272 17
        return intval($point * $this->BaseInfo->getPointConversionRate()) * -1;
273
    }
274
}
275