Skip to content

Commit 31e71b0

Browse files
authored
[Bug] Fix amount change on checkout (#346)
| Q | A | --------------- | ----- | Branch? | 1.6 | Bug fix? | yes | New feature? | no | Related tickets | n/a
2 parents a3159f7 + 353ce90 commit 31e71b0

File tree

11 files changed

+378
-0
lines changed

11 files changed

+378
-0
lines changed

UPGRADE.md

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,36 @@
3636
)
3737
```
3838

39+
`Sylius\PayPalPlugin\Controller\ProcessPayPalOrderAction`:
40+
```diff
41+
public function __construct(
42+
private readonly OrderRepositoryInterface $orderRepository,
43+
private readonly CustomerRepositoryInterface $customerRepository,
44+
private readonly FactoryInterface $customerFactory,
45+
private readonly AddressFactoryInterface $addressFactory,
46+
private readonly ObjectManager $orderManager,
47+
private readonly StateMachineFactoryInterface|StateMachineInterface $stateMachineFactory,
48+
private readonly PaymentStateManagerInterface $paymentStateManager,
49+
private readonly CacheAuthorizeClientApiInterface $authorizeClientApi,
50+
private readonly OrderDetailsApiInterface $orderDetailsApi,
51+
private readonly OrderProviderInterface $orderProvider,
52+
+ private readonly ?PaymentAmountVerifierInterface $paymentAmountVerifier = null,
53+
)
54+
```
55+
56+
`Sylius\PayPalPlugin\Controller\CompletePayPalOrderFromPaymentPageAction`:
57+
```diff
58+
public function __construct(
59+
private readonly PaymentStateManagerInterface $paymentStateManager,
60+
private readonly UrlGeneratorInterface $router,
61+
private readonly OrderProviderInterface $orderProvider,
62+
private readonly FactoryInterface|StateMachineInterface $stateMachine,
63+
private readonly ObjectManager $orderManager,
64+
+ private readonly ?PaymentAmountVerifierInterface $paymentAmountVerifier = null,
65+
+ private readonly ?OrderProcessorInterface $orderProcessor = null,
66+
)
67+
```
68+
3969
### UPGRADE FROM 1.5.1 to 1.6.0
4070

4171
1. Support for Sylius 1.13 has been added, it is now the recommended Sylius version to use.

spec/Payum/Action/CaptureActionSpec.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,7 @@ function it_authorizes_seller_send_create_order_request_and_sets_order_response_
6666
'status' => StatusAction::STATUS_CAPTURED,
6767
'paypal_order_id' => '123123',
6868
'reference_id' => 'UUID',
69+
'payment_amount' => 1000,
6970
])->shouldBeCalled();
7071

7172
$this->execute($request);

src/Controller/CompletePayPalOrderFromPaymentPageAction.php

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,11 @@
1919
use Sylius\Abstraction\StateMachine\WinzouStateMachineAdapter;
2020
use Sylius\Component\Core\Model\PaymentInterface;
2121
use Sylius\Component\Core\OrderCheckoutTransitions;
22+
use Sylius\Component\Order\Processor\OrderProcessorInterface;
23+
use Sylius\PayPalPlugin\Exception\PaymentAmountMismatchException;
2224
use Sylius\PayPalPlugin\Manager\PaymentStateManagerInterface;
2325
use Sylius\PayPalPlugin\Provider\OrderProviderInterface;
26+
use Sylius\PayPalPlugin\Verifier\PaymentAmountVerifierInterface;
2427
use Symfony\Component\HttpFoundation\JsonResponse;
2528
use Symfony\Component\HttpFoundation\Request;
2629
use Symfony\Component\HttpFoundation\Response;
@@ -34,6 +37,8 @@ public function __construct(
3437
private readonly OrderProviderInterface $orderProvider,
3538
private readonly FactoryInterface|StateMachineInterface $stateMachine,
3639
private readonly ObjectManager $orderManager,
40+
private readonly ?PaymentAmountVerifierInterface $paymentAmountVerifier = null,
41+
private readonly ?OrderProcessorInterface $orderProcessor = null,
3742
) {
3843
if ($this->stateMachine instanceof FactoryInterface) {
3944
trigger_deprecation(
@@ -46,6 +51,22 @@ public function __construct(
4651
),
4752
);
4853
}
54+
if (null === $this->paymentAmountVerifier) {
55+
trigger_deprecation(
56+
'sylius/paypal-plugin',
57+
'1.6',
58+
'Not passing an instance of "%s" as the fifth argument is deprecated and will be prohibited in 3.0.',
59+
PaymentAmountVerifierInterface::class,
60+
);
61+
}
62+
if (null === $this->orderProcessor) {
63+
trigger_deprecation(
64+
'sylius/paypal-plugin',
65+
'1.6',
66+
'Not passing an instance of "%s" as the sixth argument is deprecated and will be prohibited in 3.0.',
67+
OrderProcessorInterface::class,
68+
);
69+
}
4970
}
5071

5172
public function __invoke(Request $request): Response
@@ -56,6 +77,26 @@ public function __invoke(Request $request): Response
5677
/** @var PaymentInterface $payment */
5778
$payment = $order->getLastPayment(PaymentInterface::STATE_PROCESSING);
5879

80+
try {
81+
if ($this->paymentAmountVerifier !== null) {
82+
$this->paymentAmountVerifier->verify($payment);
83+
} else {
84+
$this->verify($payment);
85+
}
86+
} catch (PaymentAmountMismatchException) {
87+
$this->paymentStateManager->cancel($payment);
88+
$order->removePayment($payment);
89+
90+
if (null === $this->orderProcessor) {
91+
throw new \RuntimeException('Order processor is required to process the order.');
92+
}
93+
$this->orderProcessor->process($order);
94+
95+
return new JsonResponse([
96+
'return_url' => $this->router->generate('sylius_shop_checkout_complete', [], UrlGeneratorInterface::ABSOLUTE_URL),
97+
]);
98+
}
99+
59100
$this->paymentStateManager->complete($payment);
60101

61102
$this->getStateMachine()->apply($order, OrderCheckoutTransitions::GRAPH, OrderCheckoutTransitions::TRANSITION_SELECT_PAYMENT);
@@ -78,4 +119,20 @@ private function getStateMachine(): StateMachineInterface
78119

79120
return $this->stateMachine;
80121
}
122+
123+
private function verify(PaymentInterface $payment): void
124+
{
125+
$totalAmount = $this->getTotalPaymentAmountFromPaypal($payment);
126+
127+
if ($payment->getOrder()->getTotal() !== $totalAmount) {
128+
throw new PaymentAmountMismatchException();
129+
}
130+
}
131+
132+
private function getTotalPaymentAmountFromPaypal(PaymentInterface $payment): int
133+
{
134+
$details = $payment->getDetails();
135+
136+
return $details['payment_amount'] ?? 0;
137+
}
81138
}

src/Controller/ProcessPayPalOrderAction.php

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,8 +27,10 @@
2727
use Sylius\Component\Resource\Factory\FactoryInterface;
2828
use Sylius\PayPalPlugin\Api\CacheAuthorizeClientApiInterface;
2929
use Sylius\PayPalPlugin\Api\OrderDetailsApiInterface;
30+
use Sylius\PayPalPlugin\Exception\PaymentAmountMismatchException;
3031
use Sylius\PayPalPlugin\Manager\PaymentStateManagerInterface;
3132
use Sylius\PayPalPlugin\Provider\OrderProviderInterface;
33+
use Sylius\PayPalPlugin\Verifier\PaymentAmountVerifierInterface;
3234
use Symfony\Component\HttpFoundation\JsonResponse;
3335
use Symfony\Component\HttpFoundation\Request;
3436
use Symfony\Component\HttpFoundation\Response;
@@ -46,6 +48,7 @@ public function __construct(
4648
private readonly CacheAuthorizeClientApiInterface $authorizeClientApi,
4749
private readonly OrderDetailsApiInterface $orderDetailsApi,
4850
private readonly OrderProviderInterface $orderProvider,
51+
private readonly ?PaymentAmountVerifierInterface $paymentAmountVerifier = null,
4952
) {
5053
if ($this->stateMachineFactory instanceof StateMachineFactoryInterface) {
5154
trigger_deprecation(
@@ -58,6 +61,16 @@ public function __construct(
5861
),
5962
);
6063
}
64+
if (null === $this->paymentAmountVerifier) {
65+
trigger_deprecation(
66+
'sylius/paypal-plugin',
67+
'1.6',
68+
message: sprintf(
69+
'Not passing $paymentAmountVerifier to "%s" constructor is deprecated and will be prohibited in 3.0',
70+
self::class,
71+
),
72+
);
73+
}
6174
}
6275

6376
public function __invoke(Request $request): Response
@@ -112,6 +125,18 @@ public function __invoke(Request $request): Response
112125

113126
$this->orderManager->flush();
114127

128+
try {
129+
if ($this->paymentAmountVerifier !== null) {
130+
$this->paymentAmountVerifier->verify($payment, $data);
131+
} else {
132+
$this->verify($payment, $data);
133+
}
134+
} catch (PaymentAmountMismatchException) {
135+
$this->paymentStateManager->cancel($payment);
136+
137+
return new JsonResponse(['orderID' => $order->getId()]);
138+
}
139+
115140
$this->paymentStateManager->create($payment);
116141
$this->paymentStateManager->process($payment);
117142

@@ -152,4 +177,30 @@ private function getStateMachine(): StateMachineInterface
152177

153178
return $this->stateMachineFactory;
154179
}
180+
181+
private function verify(PaymentInterface $payment, array $paypalOrderDetails): void
182+
{
183+
$totalAmount = $this->getTotalPaymentAmountFromPaypal($paypalOrderDetails);
184+
185+
if ($payment->getAmount() !== $totalAmount) {
186+
throw new PaymentAmountMismatchException();
187+
}
188+
}
189+
190+
private function getTotalPaymentAmountFromPaypal(array $paypalOrderDetails): int
191+
{
192+
if (!isset($paypalOrderDetails['purchase_units']) || !is_array($paypalOrderDetails['purchase_units'])) {
193+
return 0;
194+
}
195+
196+
$totalAmount = 0;
197+
198+
foreach ($paypalOrderDetails['purchase_units'] as $unit) {
199+
$stringAmount = $unit['amount']['value'] ?? '0';
200+
201+
$totalAmount += (int) ($stringAmount * 100);
202+
}
203+
204+
return $totalAmount;
205+
}
155206
}
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Sylius package.
5+
*
6+
* (c) Sylius Sp. z o.o.
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+
declare(strict_types=1);
13+
14+
namespace Sylius\PayPalPlugin\Exception;
15+
16+
final class PaymentAmountMismatchException extends \Exception
17+
{
18+
public function __construct()
19+
{
20+
parent::__construct('Order payment amount does not match the total amount from PayPal payment.');
21+
}
22+
}

src/Payum/Action/CaptureAction.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,7 @@ public function execute($request): void
6060
'status' => StatusAction::STATUS_CAPTURED,
6161
'paypal_order_id' => $content['id'],
6262
'reference_id' => $referenceId,
63+
'payment_amount' => $payment->getAmount(),
6364
]);
6465
}
6566
}

src/Resources/config/services.xml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -270,5 +270,8 @@
270270
<service id="Sylius\PayPalPlugin\Twig\OrderAddressExtension">
271271
<tag name="twig.extension" />
272272
</service>
273+
274+
<service id="sylius_paypal.verifier.payment_amount" class="Sylius\PayPalPlugin\Verifier\PaymentAmountVerifier" />
275+
<service id="Sylius\PayPalPlugin\Verifier\PaymentAmountVerifierInterface" alias="sylius_paypal.verifier.payment_amount" />
273276
</services>
274277
</container>

src/Resources/config/services/controller.xml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,7 @@
114114
<argument type="service" id="Sylius\PayPalPlugin\Api\CacheAuthorizeClientApiInterface" />
115115
<argument type="service" id="Sylius\PayPalPlugin\Api\OrderDetailsApiInterface" />
116116
<argument type="service" id="Sylius\PayPalPlugin\Provider\OrderProviderInterface" />
117+
<argument type="service" id="sylius_paypal.verifier.payment_amount" />
117118
</service>
118119

119120
<service id="Sylius\PayPalPlugin\Controller\UpdatePayPalOrderAction">
@@ -131,6 +132,8 @@
131132
<argument type="service" id="Sylius\PayPalPlugin\Provider\OrderProviderInterface" />
132133
<argument type="service" id="sylius_abstraction.state_machine" />
133134
<argument type="service" id="sylius.manager.order" />
135+
<argument type="service" id="sylius_paypal.verifier.payment_amount" />
136+
<argument type="service" id="sylius.order_processing.order_processor" />
134137
</service>
135138

136139
<service id="Sylius\PayPalPlugin\Controller\PayPalPaymentOnErrorAction">
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Sylius package.
5+
*
6+
* (c) Sylius Sp. z o.o.
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+
declare(strict_types=1);
13+
14+
namespace Sylius\PayPalPlugin\Verifier;
15+
16+
use Sylius\Component\Core\Model\PaymentInterface;
17+
use Sylius\PayPalPlugin\Exception\PaymentAmountMismatchException;
18+
19+
final class PaymentAmountVerifier implements PaymentAmountVerifierInterface
20+
{
21+
public function verify(PaymentInterface $payment, array $paypalOrderDetails = []): void
22+
{
23+
$totalAmount = $this->getTotalPaymentAmountFromPaypal($payment, $paypalOrderDetails);
24+
25+
if ($payment->getOrder()->getTotal() !== $totalAmount) {
26+
throw new PaymentAmountMismatchException();
27+
}
28+
}
29+
30+
private function getTotalPaymentAmountFromPaypal(PaymentInterface $payment, array $paypalOrderDetails = []): int
31+
{
32+
if (empty($paypalOrderDetails)) {
33+
return $this->getPaymentAmountFromDetails($payment);
34+
}
35+
36+
if (!isset($paypalOrderDetails['purchase_units']) || !is_array($paypalOrderDetails['purchase_units'])) {
37+
return 0;
38+
}
39+
40+
$totalAmount = 0;
41+
42+
foreach ($paypalOrderDetails['purchase_units'] as $unit) {
43+
$stringAmount = $unit['amount']['value'] ?? '0';
44+
$totalAmount += (int) ($stringAmount * 100);
45+
}
46+
47+
return $totalAmount;
48+
}
49+
50+
private function getPaymentAmountFromDetails(PaymentInterface $payment): int
51+
{
52+
$details = $payment->getDetails();
53+
54+
return $details['payment_amount'] ?? 0;
55+
}
56+
}
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Sylius package.
5+
*
6+
* (c) Sylius Sp. z o.o.
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+
declare(strict_types=1);
13+
14+
namespace Sylius\PayPalPlugin\Verifier;
15+
16+
use Sylius\Component\Core\Model\PaymentInterface;
17+
18+
interface PaymentAmountVerifierInterface
19+
{
20+
public function verify(PaymentInterface $payment, array $paypalOrderDetails = []): void;
21+
}

0 commit comments

Comments
 (0)