From 6e79db51fd347a2535df1a8380ebd9f2c01a6108 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=81ukasz=20Chru=C5=9Bciel?= Date: Fri, 18 Oct 2024 20:40:41 +0200 Subject: [PATCH 01/20] [Cards] Saving credit cards during checkout --- src/Form/Type/TpayPaymentDetailsType.php | 6 ++ src/Model/PaymentDetails.php | 13 +++ .../Api/CreateCardTransactionAction.php | 4 +- .../CreateCardPaymentPayloadFactory.php | 10 +- ...eateCardPaymentPayloadFactoryInterface.php | 2 +- templates/shop/payment/_card.html.twig | 6 +- tests/Helper/PaymentDetailsHelperTrait.php | 1 + .../Api/CreateCardTransactionActionTest.php | 98 ++++++++++++++++++- .../CreateCardPaymentPayloadFactoryTest.php | 19 ++++ translations/messages.en.yaml | 2 + translations/messages.pl.yaml | 2 + 11 files changed, 158 insertions(+), 5 deletions(-) diff --git a/src/Form/Type/TpayPaymentDetailsType.php b/src/Form/Type/TpayPaymentDetailsType.php index 32231867..de36eaeb 100644 --- a/src/Form/Type/TpayPaymentDetailsType.php +++ b/src/Form/Type/TpayPaymentDetailsType.php @@ -6,6 +6,7 @@ use CommerceWeavers\SyliusTpayPlugin\Validator\Constraint\EncodedGooglePayToken; use Symfony\Component\Form\AbstractType; +use Symfony\Component\Form\Extension\Core\Type\CheckboxType; use Symfony\Component\Form\Extension\Core\Type\HiddenType; use Symfony\Component\Form\Extension\Core\Type\TelType; use Symfony\Component\Form\Extension\Core\Type\TextType; @@ -34,6 +35,11 @@ public function buildForm(FormBuilderInterface $builder, array $options): void 'property_path' => '[card]', ], ) + ->add('saveCreditCardForLater', CheckboxType::class, + [ + 'label' => 'commerce_weavers_sylius_tpay.shop.order_summary.card.save_credit_card_for_later.label', + ] + ) ->add( 'blik_token', TextType::class, diff --git a/src/Model/PaymentDetails.php b/src/Model/PaymentDetails.php index 9e867234..7172fc00 100644 --- a/src/Model/PaymentDetails.php +++ b/src/Model/PaymentDetails.php @@ -23,6 +23,7 @@ public function __construct( private ?string $googlePayToken = null, #[\SensitiveParameter] private ?string $encodedCardData = null, + private ?bool $saveCreditCardForLater = null, #[\SensitiveParameter] private ?string $applePaySession = null, private ?string $paymentUrl = null, @@ -125,6 +126,16 @@ public function setEncodedCardData(string $encodedCardData): void $this->encodedCardData = $encodedCardData; } + public function isSaveCreditCardForLater(): ?bool + { + return $this->saveCreditCardForLater; + } + + public function setSaveCreditCardForLater(?bool $saveCreditCardForLater): void + { + $this->saveCreditCardForLater = $saveCreditCardForLater; + } + public function getApplePaySession(): ?string { return $this->applePaySession; @@ -233,6 +244,7 @@ public static function fromArray(array $details): self $details['tpay']['blik_alias_application_code'] ?? null, $details['tpay']['google_pay_token'] ?? null, $details['tpay']['card'] ?? null, + $details['tpay']['saveCreditCardForLater'] ?? false, $details['tpay']['apple_pay_session'] ?? null, $details['tpay']['payment_url'] ?? null, $details['tpay']['success_url'] ?? null, @@ -256,6 +268,7 @@ public function toArray(): array 'blik_alias_application_code' => $this->blikAliasApplicationCode, 'google_pay_token' => $this->googlePayToken, 'card' => $this->encodedCardData, + 'saveCreditCardForLater' => $this->saveCreditCardForLater, 'apple_pay_session' => $this->applePaySession, 'payment_url' => $this->paymentUrl, 'success_url' => $this->successUrl, diff --git a/src/Payum/Action/Api/CreateCardTransactionAction.php b/src/Payum/Action/Api/CreateCardTransactionAction.php index e33ec8b4..26894164 100644 --- a/src/Payum/Action/Api/CreateCardTransactionAction.php +++ b/src/Payum/Action/Api/CreateCardTransactionAction.php @@ -32,9 +32,11 @@ protected function doExecute(Generic $request, PaymentInterface $model, PaymentD { $notifyToken = $this->notifyTokenFactory->create($model, $gatewayName, $localeCode); + $paymentDetails = PaymentDetails::fromArray($model->getDetails()); + $this->do( fn () => $this->api->transactions()->createTransaction( - $this->createCardPaymentPayloadFactory->createFrom($model, $notifyToken->getTargetUrl(), $localeCode), + $this->createCardPaymentPayloadFactory->createFrom($model, $notifyToken->getTargetUrl(), $localeCode, $paymentDetails->isSaveCreditCardForLater()), ), onSuccess: function (array $response) use ($paymentDetails) { $paymentDetails->setTransactionId($response['transactionId']); diff --git a/src/Tpay/Factory/CreateCardPaymentPayloadFactory.php b/src/Tpay/Factory/CreateCardPaymentPayloadFactory.php index 862b7fad..4134215e 100644 --- a/src/Tpay/Factory/CreateCardPaymentPayloadFactory.php +++ b/src/Tpay/Factory/CreateCardPaymentPayloadFactory.php @@ -6,6 +6,7 @@ use CommerceWeavers\SyliusTpayPlugin\Tpay\PayGroup; use Sylius\Component\Core\Model\PaymentInterface; +use Symfony\Component\VarDumper\VarDumper; final class CreateCardPaymentPayloadFactory implements CreateCardPaymentPayloadFactoryInterface { @@ -17,13 +18,20 @@ public function __construct( /** * @inheritDoc */ - public function createFrom(PaymentInterface $payment, string $notifyUrl, string $localeCode): array + public function createFrom(PaymentInterface $payment, string $notifyUrl, string $localeCode, bool $tokenizeCard = false): array { /** @var array{pay: array} $payload */ $payload = $this->createRedirectBasedPaymentPayloadFactory->createFrom($payment, $notifyUrl, $localeCode); $payload['pay']['groupId'] = PayGroup::CARD; + if ($tokenizeCard) { + $payload['pay']['cardPaymentData']['save'] = true; + } + + VarDumper::dump($payload); + VarDumper::dump($payment); + return $payload; } } diff --git a/src/Tpay/Factory/CreateCardPaymentPayloadFactoryInterface.php b/src/Tpay/Factory/CreateCardPaymentPayloadFactoryInterface.php index 94aa39f5..b51e2226 100644 --- a/src/Tpay/Factory/CreateCardPaymentPayloadFactoryInterface.php +++ b/src/Tpay/Factory/CreateCardPaymentPayloadFactoryInterface.php @@ -11,5 +11,5 @@ interface CreateCardPaymentPayloadFactoryInterface /** * @return array */ - public function createFrom(PaymentInterface $payment, string $notifyUrl, string $localeCode): array; + public function createFrom(PaymentInterface $payment, string $notifyUrl, string $localeCode, bool $tokenizeCard = false): array; } diff --git a/templates/shop/payment/_card.html.twig b/templates/shop/payment/_card.html.twig index 292281f1..388fd9e8 100644 --- a/templates/shop/payment/_card.html.twig +++ b/templates/shop/payment/_card.html.twig @@ -18,7 +18,7 @@
-
+
{{ form_row(form.tpay.card.cvv, { attr: { 'data-validation-error': 'commerce_weavers_sylius_tpay.shop.pay.card.cvc'|trans({}, 'validators'), 'data-tpay-cvc': '' @@ -43,6 +43,10 @@
+
+ {{ form_row(form.tpay.saveCreditCardForLater) }} +
+
{{ form_row(form.tpay.card.card, { attr: {'data-tpay-encrypted-card': '' } }) }} diff --git a/tests/Helper/PaymentDetailsHelperTrait.php b/tests/Helper/PaymentDetailsHelperTrait.php index 03ab8c0e..17de4798 100644 --- a/tests/Helper/PaymentDetailsHelperTrait.php +++ b/tests/Helper/PaymentDetailsHelperTrait.php @@ -19,6 +19,7 @@ protected function getExpectedDetails(...$overriddenDetails): array 'blik_alias_application_code' => null, 'google_pay_token' => null, 'card' => null, + 'saveCreditCardForLater' => false, 'apple_pay_session' => null, 'payment_url' => null, 'success_url' => null, diff --git a/tests/Unit/Payum/Action/Api/CreateCardTransactionActionTest.php b/tests/Unit/Payum/Action/Api/CreateCardTransactionActionTest.php index 01d7cd6b..c4799652 100644 --- a/tests/Unit/Payum/Action/Api/CreateCardTransactionActionTest.php +++ b/tests/Unit/Payum/Action/Api/CreateCardTransactionActionTest.php @@ -130,15 +130,111 @@ public function test_it_creates_a_payment_and_requests_paying_it_with_a_provided $this->api->transactions()->willReturn($transactions); +<<<<<<< HEAD $payment->setDetails( $this->getExpectedDetails(transaction_id: 'tr4ns4ct!0n_id', payment_url: 'https://tpay.org/pay'), )->shouldBeCalled(); +||||||| parent of 99e1d52 ([Cards] Saving credit cards during checkout) + $payment->setDetails([ + 'tpay' => [ + 'transaction_id' => 'tr4ns4ct!0n_id', + 'result' => null, + 'status' => null, + 'apple_pay_token' => null, + 'blik_token' => null, + 'google_pay_token' => null, + 'card' => null, + 'payment_url' => 'https://tpay.org/pay', + 'success_url' => null, + 'failure_url' => null, + 'tpay_channel_id' => null, + 'visa_mobile_phone_number' => null, + ], + ])->shouldBeCalled(); +======= + $payment->setDetails([ + 'tpay' => [ + 'transaction_id' => 'tr4ns4ct!0n_id', + 'result' => null, + 'status' => null, + 'apple_pay_token' => null, + 'blik_token' => null, + 'google_pay_token' => null, + 'card' => null, + "saveCreditCardForLater" => false, + 'payment_url' => 'https://tpay.org/pay', + 'success_url' => null, + 'failure_url' => null, + 'tpay_channel_id' => null, + 'visa_mobile_phone_number' => null, + ], + ])->shouldBeCalled(); +>>>>>>> 99e1d52 ([Cards] Saving credit cards during checkout) $this->notifyTokenFactory->create($payment, 'tpay', 'pl_PL')->willReturn($notifyToken); $this->createCardPaymentPayloadFactory - ->createFrom($payment, 'https://cw.org/notify', 'pl_PL') + ->createFrom($payment, 'https://cw.org/notify', 'pl_PL', false) + ->willReturn(['factored' => 'payload']) + ->shouldBeCalled() + ; + + $this->gateway->execute(Argument::that(function (PayWithCard $request) use ($token): bool { + return $request->getToken() === $token->reveal(); + }))->shouldBeCalled(); + + $this->createTestSubject()->execute($request->reveal()); + } + + public function test_it_creates_a_payment_and_requests_paying_it_with_a_provided_card_that_will_be_saved_for_later(): void + { + $order = $this->prophesize(OrderInterface::class); + $order->getLocaleCode()->willReturn('pl_PL'); + + $payment = $this->prophesize(PaymentInterface::class); + $payment->getOrder()->willReturn($order); + $payment->getDetails()->willReturn(['tpay' => ['saveCreditCardForLater' => true]]); + + $token = $this->prophesize(TokenInterface::class); + $token->getGatewayName()->willReturn('tpay'); + + $request = $this->prophesize(CreateTransaction::class); + $request->getModel()->willReturn($payment); + $request->getToken()->willReturn($token); + + $notifyToken = $this->prophesize(TokenInterface::class); + $notifyToken->getTargetUrl()->willReturn('https://cw.org/notify'); + + $transactions = $this->prophesize(TransactionsApi::class); + $transactions->createTransaction(['factored' => 'payload'])->willReturn([ + 'transactionId' => 'tr4ns4ct!0n_id', + 'transactionPaymentUrl' => 'https://tpay.org/pay', + ]); + + $this->api->transactions()->willReturn($transactions); + + $payment->setDetails([ + 'tpay' => [ + 'transaction_id' => 'tr4ns4ct!0n_id', + 'result' => null, + 'status' => null, + 'blik_token' => null, + 'google_pay_token' => null, + 'card' => null, + 'saveCreditCardForLater' => true, + 'payment_url' => 'https://tpay.org/pay', + 'success_url' => null, + 'failure_url' => null, + 'tpay_channel_id' => null + ], + ])->shouldBeCalled(); + + $this->notifyTokenFactory->create($payment, 'tpay', 'pl_PL')->willReturn($notifyToken); + + $this->createCardPaymentPayloadFactory + ->createFrom($payment, 'https://cw.org/notify', 'pl_PL', true) ->willReturn(['factored' => 'payload']) + ->shouldBeCalled() ; $this->gateway->execute(Argument::that(function (PayWithCard $request) use ($payment): bool { diff --git a/tests/Unit/Tpay/Factory/CreateCardPaymentPayloadFactoryTest.php b/tests/Unit/Tpay/Factory/CreateCardPaymentPayloadFactoryTest.php index f5e40b79..b39ce7c4 100644 --- a/tests/Unit/Tpay/Factory/CreateCardPaymentPayloadFactoryTest.php +++ b/tests/Unit/Tpay/Factory/CreateCardPaymentPayloadFactoryTest.php @@ -39,6 +39,25 @@ public function test_it_adds_card_related_data_to_a_basic_create_payment_payload ], $payload); } + public function test_it_adds_card_related_data_with_tokenization_request_to_a_basic_create_payment_payload_output(): void + { + $payment = $this->prophesize(PaymentInterface::class); + + $this->createRedirectBasedPaymentPayloadFactory->createFrom($payment, 'https://cw.org/notify', 'pl_PL')->willReturn(['some' => 'data']); + + $payload = $this->createTestSubject()->createFrom($payment->reveal(), 'https://cw.org/notify', 'pl_PL', true); + + $this->assertSame([ + 'some' => 'data', + 'pay' => [ + 'groupId' => 103, + 'cardPaymentData' => [ + 'save' => 1, + ], + ], + ], $payload); + } + private function createTestSubject(): CreateCardPaymentPayloadFactoryInterface { return new CreateCardPaymentPayloadFactory($this->createRedirectBasedPaymentPayloadFactory->reveal()); diff --git a/translations/messages.en.yaml b/translations/messages.en.yaml index 933733e5..ee7dbd32 100644 --- a/translations/messages.en.yaml +++ b/translations/messages.en.yaml @@ -44,6 +44,8 @@ commerce_weavers_sylius_tpay: year_placeholder: 'Choose a year' holder_name: 'Cardholder name' number: 'Number' + save_credit_card_for_later: + label: 'Save card for later' visa_mobile: placeholder: 'Phone number' payment_failed: diff --git a/translations/messages.pl.yaml b/translations/messages.pl.yaml index e3f0168f..489ca428 100644 --- a/translations/messages.pl.yaml +++ b/translations/messages.pl.yaml @@ -44,6 +44,8 @@ commerce_weavers_sylius_tpay: year_placeholder: 'Wybierz rok' holder_name: 'Imię i nazwisko właściciela' number: 'Numer' + save_credit_card_for_later: + label: 'Zapisz kartę na później' visa_mobile: placeholder: 'Numer telefonu' payment_failed: From d255cdd55bbd7a9f95afba68572496391e24fd24 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=81ukasz=20Chru=C5=9Bciel?= Date: Tue, 22 Oct 2024 16:37:44 +0200 Subject: [PATCH 02/20] Move saving card flag to transaction closing endpoint --- src/Model/PaymentDetails.php | 4 +- .../Api/CreateCardTransactionAction.php | 2 +- src/Payum/Action/Api/PayWithCardAction.php | 18 ++-- .../CreateCardPaymentPayloadFactory.php | 10 +- ...eateCardPaymentPayloadFactoryInterface.php | 2 +- .../Api/CreateCardTransactionActionTest.php | 97 +------------------ .../Action/Api/PayWithCardActionTest.php | 58 ++++++++++- .../CreateCardPaymentPayloadFactoryTest.php | 22 +---- 8 files changed, 77 insertions(+), 136 deletions(-) diff --git a/src/Model/PaymentDetails.php b/src/Model/PaymentDetails.php index 7172fc00..637bd5e7 100644 --- a/src/Model/PaymentDetails.php +++ b/src/Model/PaymentDetails.php @@ -23,7 +23,7 @@ public function __construct( private ?string $googlePayToken = null, #[\SensitiveParameter] private ?string $encodedCardData = null, - private ?bool $saveCreditCardForLater = null, + private bool $saveCreditCardForLater = false, #[\SensitiveParameter] private ?string $applePaySession = null, private ?string $paymentUrl = null, @@ -126,7 +126,7 @@ public function setEncodedCardData(string $encodedCardData): void $this->encodedCardData = $encodedCardData; } - public function isSaveCreditCardForLater(): ?bool + public function isSaveCreditCardForLater(): bool { return $this->saveCreditCardForLater; } diff --git a/src/Payum/Action/Api/CreateCardTransactionAction.php b/src/Payum/Action/Api/CreateCardTransactionAction.php index 26894164..692adfb3 100644 --- a/src/Payum/Action/Api/CreateCardTransactionAction.php +++ b/src/Payum/Action/Api/CreateCardTransactionAction.php @@ -36,7 +36,7 @@ protected function doExecute(Generic $request, PaymentInterface $model, PaymentD $this->do( fn () => $this->api->transactions()->createTransaction( - $this->createCardPaymentPayloadFactory->createFrom($model, $notifyToken->getTargetUrl(), $localeCode, $paymentDetails->isSaveCreditCardForLater()), + $this->createCardPaymentPayloadFactory->createFrom($model, $notifyToken->getTargetUrl(), $localeCode), ), onSuccess: function (array $response) use ($paymentDetails) { $paymentDetails->setTransactionId($response['transactionId']); diff --git a/src/Payum/Action/Api/PayWithCardAction.php b/src/Payum/Action/Api/PayWithCardAction.php index 1434642d..0608db0f 100644 --- a/src/Payum/Action/Api/PayWithCardAction.php +++ b/src/Payum/Action/Api/PayWithCardAction.php @@ -19,13 +19,19 @@ protected function doExecute(Generic $request, PaymentInterface $model, PaymentD Assert::notNull($paymentDetails->getEncodedCardData(), 'Card data is required to pay with card.'); Assert::notNull($paymentDetails->getTransactionId(), 'Transaction ID is required to pay with card.'); + $payload = [ + 'groupId' => PayGroup::CARD, + 'cardPaymentData' => [ + 'card' => $paymentDetails->getEncodedCardData(), + ], + ]; + + if ($paymentDetails->isSaveCreditCardForLater()) { + $payload['cardPaymentData']['save'] = true; + } + $this->do( - fn () => $this->api->transactions()->createPaymentByTransactionId([ - 'groupId' => PayGroup::CARD, - 'cardPaymentData' => [ - 'card' => $paymentDetails->getEncodedCardData(), - ], - ], $paymentDetails->getTransactionId()), + fn () => $this->api->transactions()->createPaymentByTransactionId($payload, $paymentDetails->getTransactionId()), onSuccess: function ($response) use ($paymentDetails) { $paymentDetails->setResult($response['result']); $paymentDetails->setStatus($response['status']); diff --git a/src/Tpay/Factory/CreateCardPaymentPayloadFactory.php b/src/Tpay/Factory/CreateCardPaymentPayloadFactory.php index 4134215e..862b7fad 100644 --- a/src/Tpay/Factory/CreateCardPaymentPayloadFactory.php +++ b/src/Tpay/Factory/CreateCardPaymentPayloadFactory.php @@ -6,7 +6,6 @@ use CommerceWeavers\SyliusTpayPlugin\Tpay\PayGroup; use Sylius\Component\Core\Model\PaymentInterface; -use Symfony\Component\VarDumper\VarDumper; final class CreateCardPaymentPayloadFactory implements CreateCardPaymentPayloadFactoryInterface { @@ -18,20 +17,13 @@ public function __construct( /** * @inheritDoc */ - public function createFrom(PaymentInterface $payment, string $notifyUrl, string $localeCode, bool $tokenizeCard = false): array + public function createFrom(PaymentInterface $payment, string $notifyUrl, string $localeCode): array { /** @var array{pay: array} $payload */ $payload = $this->createRedirectBasedPaymentPayloadFactory->createFrom($payment, $notifyUrl, $localeCode); $payload['pay']['groupId'] = PayGroup::CARD; - if ($tokenizeCard) { - $payload['pay']['cardPaymentData']['save'] = true; - } - - VarDumper::dump($payload); - VarDumper::dump($payment); - return $payload; } } diff --git a/src/Tpay/Factory/CreateCardPaymentPayloadFactoryInterface.php b/src/Tpay/Factory/CreateCardPaymentPayloadFactoryInterface.php index b51e2226..94aa39f5 100644 --- a/src/Tpay/Factory/CreateCardPaymentPayloadFactoryInterface.php +++ b/src/Tpay/Factory/CreateCardPaymentPayloadFactoryInterface.php @@ -11,5 +11,5 @@ interface CreateCardPaymentPayloadFactoryInterface /** * @return array */ - public function createFrom(PaymentInterface $payment, string $notifyUrl, string $localeCode, bool $tokenizeCard = false): array; + public function createFrom(PaymentInterface $payment, string $notifyUrl, string $localeCode): array; } diff --git a/tests/Unit/Payum/Action/Api/CreateCardTransactionActionTest.php b/tests/Unit/Payum/Action/Api/CreateCardTransactionActionTest.php index c4799652..3a4b7600 100644 --- a/tests/Unit/Payum/Action/Api/CreateCardTransactionActionTest.php +++ b/tests/Unit/Payum/Action/Api/CreateCardTransactionActionTest.php @@ -130,109 +130,14 @@ public function test_it_creates_a_payment_and_requests_paying_it_with_a_provided $this->api->transactions()->willReturn($transactions); -<<<<<<< HEAD $payment->setDetails( $this->getExpectedDetails(transaction_id: 'tr4ns4ct!0n_id', payment_url: 'https://tpay.org/pay'), )->shouldBeCalled(); -||||||| parent of 99e1d52 ([Cards] Saving credit cards during checkout) - $payment->setDetails([ - 'tpay' => [ - 'transaction_id' => 'tr4ns4ct!0n_id', - 'result' => null, - 'status' => null, - 'apple_pay_token' => null, - 'blik_token' => null, - 'google_pay_token' => null, - 'card' => null, - 'payment_url' => 'https://tpay.org/pay', - 'success_url' => null, - 'failure_url' => null, - 'tpay_channel_id' => null, - 'visa_mobile_phone_number' => null, - ], - ])->shouldBeCalled(); -======= - $payment->setDetails([ - 'tpay' => [ - 'transaction_id' => 'tr4ns4ct!0n_id', - 'result' => null, - 'status' => null, - 'apple_pay_token' => null, - 'blik_token' => null, - 'google_pay_token' => null, - 'card' => null, - "saveCreditCardForLater" => false, - 'payment_url' => 'https://tpay.org/pay', - 'success_url' => null, - 'failure_url' => null, - 'tpay_channel_id' => null, - 'visa_mobile_phone_number' => null, - ], - ])->shouldBeCalled(); ->>>>>>> 99e1d52 ([Cards] Saving credit cards during checkout) $this->notifyTokenFactory->create($payment, 'tpay', 'pl_PL')->willReturn($notifyToken); $this->createCardPaymentPayloadFactory - ->createFrom($payment, 'https://cw.org/notify', 'pl_PL', false) - ->willReturn(['factored' => 'payload']) - ->shouldBeCalled() - ; - - $this->gateway->execute(Argument::that(function (PayWithCard $request) use ($token): bool { - return $request->getToken() === $token->reveal(); - }))->shouldBeCalled(); - - $this->createTestSubject()->execute($request->reveal()); - } - - public function test_it_creates_a_payment_and_requests_paying_it_with_a_provided_card_that_will_be_saved_for_later(): void - { - $order = $this->prophesize(OrderInterface::class); - $order->getLocaleCode()->willReturn('pl_PL'); - - $payment = $this->prophesize(PaymentInterface::class); - $payment->getOrder()->willReturn($order); - $payment->getDetails()->willReturn(['tpay' => ['saveCreditCardForLater' => true]]); - - $token = $this->prophesize(TokenInterface::class); - $token->getGatewayName()->willReturn('tpay'); - - $request = $this->prophesize(CreateTransaction::class); - $request->getModel()->willReturn($payment); - $request->getToken()->willReturn($token); - - $notifyToken = $this->prophesize(TokenInterface::class); - $notifyToken->getTargetUrl()->willReturn('https://cw.org/notify'); - - $transactions = $this->prophesize(TransactionsApi::class); - $transactions->createTransaction(['factored' => 'payload'])->willReturn([ - 'transactionId' => 'tr4ns4ct!0n_id', - 'transactionPaymentUrl' => 'https://tpay.org/pay', - ]); - - $this->api->transactions()->willReturn($transactions); - - $payment->setDetails([ - 'tpay' => [ - 'transaction_id' => 'tr4ns4ct!0n_id', - 'result' => null, - 'status' => null, - 'blik_token' => null, - 'google_pay_token' => null, - 'card' => null, - 'saveCreditCardForLater' => true, - 'payment_url' => 'https://tpay.org/pay', - 'success_url' => null, - 'failure_url' => null, - 'tpay_channel_id' => null - ], - ])->shouldBeCalled(); - - $this->notifyTokenFactory->create($payment, 'tpay', 'pl_PL')->willReturn($notifyToken); - - $this->createCardPaymentPayloadFactory - ->createFrom($payment, 'https://cw.org/notify', 'pl_PL', true) + ->createFrom($payment, 'https://cw.org/notify', 'pl_PL') ->willReturn(['factored' => 'payload']) ->shouldBeCalled() ; diff --git a/tests/Unit/Payum/Action/Api/PayWithCardActionTest.php b/tests/Unit/Payum/Action/Api/PayWithCardActionTest.php index 81cf4270..ea3518e4 100644 --- a/tests/Unit/Payum/Action/Api/PayWithCardActionTest.php +++ b/tests/Unit/Payum/Action/Api/PayWithCardActionTest.php @@ -59,7 +59,6 @@ public function test_it_does_not_supports_pay_with_card_request_with_non_payment $this->assertFalse($isSupported); } - public function test_it_redirects_a_customer_to_3ds_verification_once_a_transaction_status_is_pending(): void { $this->expectException(HttpRedirect::class); @@ -152,6 +151,63 @@ public function test_it_marks_payment_as_failed_if_tpay_throws_an_exception(): v $subject->execute($request->reveal()); } + public function test_it_redirects_a_customer_to_3ds_verification_and_save_a_card(): void + { + $this->expectException(HttpRedirect::class); + + $request = $this->prophesize(PayWithCard::class); + $paymentModel = $this->prophesize(PaymentInterface::class); + $details = [ + 'tpay' => [ + 'card' => 'test-card', + 'transaction_id' => 'abcd', + 'saveCreditCardForLater' => true, + ], + ]; + + $response = [ + 'result' => 'success', + 'status' => 'pending', + 'transactionPaymentUrl' => 'http://example.com', + ]; + + $request->getModel()->willReturn($paymentModel->reveal()); + $paymentModel->getDetails()->willReturn($details); + + $transactions = $this->prophesize(TransactionsApi::class); + $transactions->createPaymentByTransactionId([ + 'groupId' => 103, + 'cardPaymentData' => [ + 'card' => $details['tpay']['card'], + 'save' => true, + ], + ], $details['tpay']['transaction_id'])->willReturn($response); + + $this->api->transactions()->willReturn($transactions); + + $paymentModel->setDetails([ + 'tpay' => [ + 'transaction_id' => 'abcd', + 'result' => 'success', + 'status' => 'pending', + 'apple_pay_token' => null, + 'blik_token' => null, + 'google_pay_token' => null, + 'card' => null, + 'saveCreditCardForLater' => true, + 'payment_url' => 'http://example.com', + 'success_url' => null, + 'failure_url' => null, + 'tpay_channel_id' => null, + 'visa_mobile_phone_number' => null, + ], + ])->shouldBeCalled(); + + $subject = $this->createTestSubject(); + + $subject->execute($request->reveal()); + } + public function test_it_marks_a_payment_status_as_failed_once_a_transaction_status_is_failed(): void { $details = [ diff --git a/tests/Unit/Tpay/Factory/CreateCardPaymentPayloadFactoryTest.php b/tests/Unit/Tpay/Factory/CreateCardPaymentPayloadFactoryTest.php index b39ce7c4..4ffe459c 100644 --- a/tests/Unit/Tpay/Factory/CreateCardPaymentPayloadFactoryTest.php +++ b/tests/Unit/Tpay/Factory/CreateCardPaymentPayloadFactoryTest.php @@ -7,6 +7,7 @@ use CommerceWeavers\SyliusTpayPlugin\Tpay\Factory\CreateCardPaymentPayloadFactory; use CommerceWeavers\SyliusTpayPlugin\Tpay\Factory\CreateCardPaymentPayloadFactoryInterface; use CommerceWeavers\SyliusTpayPlugin\Tpay\Factory\CreateRedirectBasedPaymentPayloadFactoryInterface; +use CommerceWeavers\SyliusTpayPlugin\Tpay\PayGroup; use PHPUnit\Framework\TestCase; use Prophecy\PhpUnit\ProphecyTrait; use Prophecy\Prophecy\ObjectProphecy; @@ -34,26 +35,7 @@ public function test_it_adds_card_related_data_to_a_basic_create_payment_payload $this->assertSame([ 'some' => 'data', 'pay' => [ - 'groupId' => 103, - ], - ], $payload); - } - - public function test_it_adds_card_related_data_with_tokenization_request_to_a_basic_create_payment_payload_output(): void - { - $payment = $this->prophesize(PaymentInterface::class); - - $this->createRedirectBasedPaymentPayloadFactory->createFrom($payment, 'https://cw.org/notify', 'pl_PL')->willReturn(['some' => 'data']); - - $payload = $this->createTestSubject()->createFrom($payment->reveal(), 'https://cw.org/notify', 'pl_PL', true); - - $this->assertSame([ - 'some' => 'data', - 'pay' => [ - 'groupId' => 103, - 'cardPaymentData' => [ - 'save' => 1, - ], + 'groupId' => PayGroup::CARD, ], ], $payload); } From bc8c5bc2f1fad6a13b7836a9ee0742a1fe2722dd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=81ukasz=20Chru=C5=9Bciel?= Date: Wed, 23 Oct 2024 11:42:29 +0200 Subject: [PATCH 03/20] Allow card saving only for registered users --- config/services/form.php | 1 + src/Form/Type/TpayPaymentDetailsType.php | 21 +++++-- templates/shop/payment/_card.html.twig | 10 +-- .../Checkout/TpayCreditCardCheckoutTest.php | 63 +++++++++++++++++++ tests/E2E/Helper/Order/TpayTrait.php | 6 +- 5 files changed, 91 insertions(+), 10 deletions(-) create mode 100644 tests/E2E/Checkout/TpayCreditCardCheckoutTest.php diff --git a/config/services/form.php b/config/services/form.php index 1a3abf54..97ce24ad 100644 --- a/config/services/form.php +++ b/config/services/form.php @@ -49,6 +49,7 @@ $services->set(TpayPaymentDetailsType::class) ->args([ service('commerce_weavers_sylius_tpay.form.event_listener.remove_unnecessary_payment_details_fields'), + service('security.token_storage'), ]) ->tag('form.type') ; diff --git a/src/Form/Type/TpayPaymentDetailsType.php b/src/Form/Type/TpayPaymentDetailsType.php index de36eaeb..d11d6aaf 100644 --- a/src/Form/Type/TpayPaymentDetailsType.php +++ b/src/Form/Type/TpayPaymentDetailsType.php @@ -5,6 +5,7 @@ namespace CommerceWeavers\SyliusTpayPlugin\Form\Type; use CommerceWeavers\SyliusTpayPlugin\Validator\Constraint\EncodedGooglePayToken; +use Sylius\Component\Core\Model\ShopUserInterface; use Symfony\Component\Form\AbstractType; use Symfony\Component\Form\Extension\Core\Type\CheckboxType; use Symfony\Component\Form\Extension\Core\Type\HiddenType; @@ -12,6 +13,7 @@ use Symfony\Component\Form\Extension\Core\Type\TextType; use Symfony\Component\Form\FormBuilderInterface; use Symfony\Component\Form\FormEvents; +use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface; use Symfony\Component\Validator\Constraints\Length; use Symfony\Component\Validator\Constraints\Regex; @@ -19,6 +21,7 @@ final class TpayPaymentDetailsType extends AbstractType { public function __construct( private readonly object $removeUnnecessaryPaymentDetailsFieldsListener, + private readonly TokenStorageInterface $tokenStorage, ) { } @@ -35,11 +38,6 @@ public function buildForm(FormBuilderInterface $builder, array $options): void 'property_path' => '[card]', ], ) - ->add('saveCreditCardForLater', CheckboxType::class, - [ - 'label' => 'commerce_weavers_sylius_tpay.shop.order_summary.card.save_credit_card_for_later.label', - ] - ) ->add( 'blik_token', TextType::class, @@ -107,5 +105,18 @@ public function buildForm(FormBuilderInterface $builder, array $options): void FormEvents::PRE_SUBMIT, [$this->removeUnnecessaryPaymentDetailsFieldsListener, '__invoke'], ); + + $token = $this->tokenStorage->getToken(); + $user = $token?->getUser(); + + if ($user instanceof ShopUserInterface) { + $builder + ->add('saveCreditCardForLater', CheckboxType::class, + [ + 'label' => 'commerce_weavers_sylius_tpay.shop.order_summary.card.save_credit_card_for_later.label', + ] + ) + ; + } } } diff --git a/templates/shop/payment/_card.html.twig b/templates/shop/payment/_card.html.twig index 388fd9e8..6d84dce8 100644 --- a/templates/shop/payment/_card.html.twig +++ b/templates/shop/payment/_card.html.twig @@ -43,10 +43,12 @@ -
- {{ form_row(form.tpay.saveCreditCardForLater) }} -
-
+ {% if form.tpay.saveCreditCardForLater is defined %} +
+ {{ form_row(form.tpay.saveCreditCardForLater) }} +
+
+ {% endif %} {{ form_row(form.tpay.card.card, { attr: {'data-tpay-encrypted-card': '' } }) }} diff --git a/tests/E2E/Checkout/TpayCreditCardCheckoutTest.php b/tests/E2E/Checkout/TpayCreditCardCheckoutTest.php new file mode 100644 index 00000000..82143313 --- /dev/null +++ b/tests/E2E/Checkout/TpayCreditCardCheckoutTest.php @@ -0,0 +1,63 @@ +loadFixtures(['addressed_cart.yaml']); + + // the cart is already addressed, so we go straight to selecting a shipping method + $this->showSelectingShippingMethodStep(); + $this->processWithDefaultShippingMethod(); + } + + public function test_it_completes_the_checkout_using_credit_card(): void + { + $this->loginShopUser('tony@nonexisting.cw', 'sylius'); + + $this->processWithPaymentMethod('tpay_card'); + $this->fillCardData(self::FORM_ID, 'John Doe', self::CARD_NUMBER, '123', '01', '2029'); + $this->placeOrder(); + + $this->assertPageTitleContains('Thank you!'); + } + + public function test_it_completes_the_checkout_using_credit_card_and_saves_the_card(): void + { + $this->loginShopUser('tony@nonexisting.cw', 'sylius'); + + $this->processWithPaymentMethod('tpay_card'); + $this->fillCardData(self::FORM_ID, 'John Doe', self::CARD_NUMBER, '123', '01', '2029', true); + $this->placeOrder(); + + $this->assertPageTitleContains('Thank you!'); + } + + public function test_it_forbids_card_saving_for_not_logged_in_users(): void + { + $this->expectException(NoSuchElementException::class); + + $this->processWithPaymentMethod('tpay_card'); + $this->fillCardData(self::FORM_ID, 'John Doe', self::CARD_NUMBER, '123', '01', '2029', true); + } +} diff --git a/tests/E2E/Helper/Order/TpayTrait.php b/tests/E2E/Helper/Order/TpayTrait.php index e9274934..40398b5d 100644 --- a/tests/E2E/Helper/Order/TpayTrait.php +++ b/tests/E2E/Helper/Order/TpayTrait.php @@ -13,12 +13,16 @@ */ trait TpayTrait { - public function fillCardData(string $formId, string $cardNumber, string $cvv, string $month, string $year): void + public function fillCardData(string $formId, string $cardNumber, string $cvv, string $month, string $year, bool $saveCardForLater): void { $this->client->findElement(WebDriverBy::id(sprintf('%s_tpay_card_number', $formId)))->sendKeys($cardNumber); $this->client->findElement(WebDriverBy::id(sprintf('%s_tpay_card_cvv', $formId)))->sendKeys($cvv); $this->client->findElement(WebDriverBy::id(sprintf('%s_tpay_card_expiration_date_month', $formId)))->sendKeys($month); $this->client->findElement(WebDriverBy::id(sprintf('%s_tpay_card_expiration_date_year', $formId)))->sendKeys($year); + + if ($saveCardForLater) { + $this->client->findElement(WebDriverBy::id(sprintf('%s_tpay_saveCreditCardForLater', $formId)))->click(); + } } public function fillBlikToken(string $formId, string $blikToken): void From fe8caea1375dbc0e972452226917fc58cc1e636c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=81ukasz=20Chru=C5=9Bciel?= Date: Mon, 28 Oct 2024 22:38:33 +0100 Subject: [PATCH 04/20] Accepting card token on webhook --- config/doctrine/CreditCard.orm.xml | 23 ++++ config/services/payum/action.php | 9 ++ migrations/Version20241029160137.php | 28 +++++ phpunit.xml.dist | 3 + src/Api/Command/Pay.php | 1 + src/Api/Command/PayByCard.php | 1 + src/Api/Command/PayByCardHandler.php | 5 +- .../Factory/NextCommand/PayByCardFactory.php | 4 +- src/DependencyInjection/Configuration.php | 20 ++++ src/Entity/CreditCard.php | 74 ++++++++++++ src/Entity/CreditCardInterface.php | 29 +++++ src/Model/PaymentDetails.php | 2 +- .../Api/CreateCardTransactionAction.php | 2 - src/Payum/Action/Api/NotifyAction.php | 14 +++ src/Payum/Action/Api/SaveCreditCardAction.php | 55 +++++++++ src/Payum/Request/Api/SaveCreditCard.php | 21 ++++ .../shop/paying_for_orders_by_card/cart.yml | 10 ++ tests/Api/Shop/PayingForOrdersByCardTest.php | 66 +++++++++++ tests/Api/Utils/UserLoginTrait.php | 27 +++++ tests/Application/.env.test | 6 + tests/Application/config/jwt/private-test.pem | 51 ++++++++ tests/Application/config/jwt/public-test.pem | 14 +++ .../Checkout/TpayCreditCardCheckoutTest.php | 6 +- tests/E2E/Helper/Order/TpayTrait.php | 2 +- .../Unit/Api/Command/PayByCardHandlerTest.php | 17 +++ .../NextCommand/PayByCardFactoryTest.php | 12 +- .../Payum/Action/Api/NotifyActionTest.php | 36 ++++-- .../Action/Api/SaveCreditCardActionTest.php | 112 ++++++++++++++++++ tests/mockoon_tpay.json | 2 +- 29 files changed, 632 insertions(+), 20 deletions(-) create mode 100644 config/doctrine/CreditCard.orm.xml create mode 100644 migrations/Version20241029160137.php create mode 100644 src/Entity/CreditCard.php create mode 100644 src/Entity/CreditCardInterface.php create mode 100644 src/Payum/Action/Api/SaveCreditCardAction.php create mode 100644 src/Payum/Request/Api/SaveCreditCard.php create mode 100644 tests/Api/Utils/UserLoginTrait.php create mode 100644 tests/Application/config/jwt/private-test.pem create mode 100644 tests/Application/config/jwt/public-test.pem create mode 100644 tests/Unit/Payum/Action/Api/SaveCreditCardActionTest.php diff --git a/config/doctrine/CreditCard.orm.xml b/config/doctrine/CreditCard.orm.xml new file mode 100644 index 00000000..65b71251 --- /dev/null +++ b/config/doctrine/CreditCard.orm.xml @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + + + + + + diff --git a/config/services/payum/action.php b/config/services/payum/action.php index 790bf171..392b085c 100644 --- a/config/services/payum/action.php +++ b/config/services/payum/action.php @@ -15,6 +15,7 @@ use CommerceWeavers\SyliusTpayPlugin\Payum\Action\Api\InitializeApplePayPaymentAction; use CommerceWeavers\SyliusTpayPlugin\Payum\Action\Api\NotifyAction; use CommerceWeavers\SyliusTpayPlugin\Payum\Action\Api\PayWithCardAction; +use CommerceWeavers\SyliusTpayPlugin\Payum\Action\Api\SaveCreditCardAction; use CommerceWeavers\SyliusTpayPlugin\Payum\Action\CaptureAction; use CommerceWeavers\SyliusTpayPlugin\Payum\Action\GetStatusAction; use CommerceWeavers\SyliusTpayPlugin\Payum\Action\PartialRefundAction; @@ -93,6 +94,14 @@ ->tag('payum.action', ['factory' => TpayGatewayFactory::NAME, 'alias' => 'cw.tpay.notify']) ; + $services->set(SaveCreditCardAction::class) + ->args([ + service('commerce_weavers_sylius_tpay.factory.credit_card'), + service('commerce_weavers_sylius_tpay.repository.credit_card'), + ]) + ->tag('payum.action', ['factory' => TpayGatewayFactory::NAME, 'alias' => 'cw.tpay.credit_card']) + ; + $services->set(PayWithCardAction::class) ->tag('payum.action', ['factory' => TpayGatewayFactory::NAME, 'alias' => 'cw.tpay.pay_with_card']) ; diff --git a/migrations/Version20241029160137.php b/migrations/Version20241029160137.php new file mode 100644 index 00000000..e7cd96ea --- /dev/null +++ b/migrations/Version20241029160137.php @@ -0,0 +1,28 @@ +addSql('CREATE TABLE cw_sylius_tpay_credt_card (id INT AUTO_INCREMENT NOT NULL, customer_id INT NOT NULL, token VARCHAR(255) NOT NULL, brand VARCHAR(255) NOT NULL, tail VARCHAR(255) NOT NULL, expiration_date DATETIME DEFAULT NULL, INDEX IDX_9FF1996C9395C3F3 (customer_id), PRIMARY KEY(id)) DEFAULT CHARACTER SET UTF8 COLLATE `UTF8_unicode_ci` ENGINE = InnoDB'); + $this->addSql('ALTER TABLE cw_sylius_tpay_credt_card ADD CONSTRAINT FK_9FF1996C9395C3F3 FOREIGN KEY (customer_id) REFERENCES sylius_customer (id) ON DELETE CASCADE'); + } + + public function down(Schema $schema): void + { + $this->addSql('ALTER TABLE cw_sylius_tpay_credt_card DROP FOREIGN KEY FK_9FF1996C9395C3F3'); + $this->addSql('DROP TABLE cw_sylius_tpay_credt_card'); + } +} diff --git a/phpunit.xml.dist b/phpunit.xml.dist index 2916ea59..defe351a 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -37,6 +37,9 @@ + + + diff --git a/src/Api/Command/Pay.php b/src/Api/Command/Pay.php index 0b6490fa..130f656f 100644 --- a/src/Api/Command/Pay.php +++ b/src/Api/Command/Pay.php @@ -19,6 +19,7 @@ public function __construct( public readonly ?string $blikAliasApplicationCode = null, public readonly ?string $googlePayToken = null, public readonly ?string $encodedCardData = null, + public readonly bool $saveCard = false, public readonly ?string $tpayChannelId = null, public readonly ?string $visaMobilePhoneNumber = null, ) { diff --git a/src/Api/Command/PayByCard.php b/src/Api/Command/PayByCard.php index d909c015..5706e28a 100644 --- a/src/Api/Command/PayByCard.php +++ b/src/Api/Command/PayByCard.php @@ -9,6 +9,7 @@ final class PayByCard public function __construct( public readonly int $paymentId, public readonly string $encodedCardData, + public readonly bool $saveCard = false, ) { } } diff --git a/src/Api/Command/PayByCardHandler.php b/src/Api/Command/PayByCardHandler.php index 7b70320a..ccea37c7 100644 --- a/src/Api/Command/PayByCardHandler.php +++ b/src/Api/Command/PayByCardHandler.php @@ -15,16 +15,17 @@ public function __invoke(PayByCard $command): PayResult { $payment = $this->findOr404($command->paymentId); - $this->setTransactionData($payment, $command->encodedCardData); + $this->setTransactionData($payment, $command->encodedCardData, $command->saveCard); $this->createTransactionProcessor->process($payment); return $this->createResultFrom($payment); } - private function setTransactionData(PaymentInterface $payment, string $encodedCardData): void + private function setTransactionData(PaymentInterface $payment, string $encodedCardData, bool $saveCard = false): void { $paymentDetails = PaymentDetails::fromArray($payment->getDetails()); $paymentDetails->setEncodedCardData($encodedCardData); + $paymentDetails->setSaveCreditCardForLater($saveCard); $payment->setDetails($paymentDetails->toArray()); } diff --git a/src/Api/Factory/NextCommand/PayByCardFactory.php b/src/Api/Factory/NextCommand/PayByCardFactory.php index 005b04fa..a5df13b3 100644 --- a/src/Api/Factory/NextCommand/PayByCardFactory.php +++ b/src/Api/Factory/NextCommand/PayByCardFactory.php @@ -23,7 +23,9 @@ public function create(Pay $command, PaymentInterface $payment): PayByCard /** @var string $encodedCardData */ $encodedCardData = $command->encodedCardData; - return new PayByCard($paymentId, $encodedCardData); + $saveCard = $command->saveCard; + + return new PayByCard($paymentId, $encodedCardData, $saveCard); } public function supports(Pay $command, PaymentInterface $payment): bool diff --git a/src/DependencyInjection/Configuration.php b/src/DependencyInjection/Configuration.php index b82a0dd4..2bd3d368 100644 --- a/src/DependencyInjection/Configuration.php +++ b/src/DependencyInjection/Configuration.php @@ -6,9 +6,13 @@ use CommerceWeavers\SyliusTpayPlugin\Entity\BlikAlias; use CommerceWeavers\SyliusTpayPlugin\Entity\BlikAliasInterface; +use CommerceWeavers\SyliusTpayPlugin\Entity\CreditCard; +use CommerceWeavers\SyliusTpayPlugin\Entity\CreditCardInterface; use CommerceWeavers\SyliusTpayPlugin\Factory\BlikAliasFactory; use CommerceWeavers\SyliusTpayPlugin\Repository\BlikAliasRepository; use Sylius\Bundle\ResourceBundle\Controller\ResourceController; +use Sylius\Bundle\ResourceBundle\Doctrine\ORM\EntityRepository; +use Sylius\Resource\Factory\Factory; use Symfony\Component\Config\Definition\Builder\ArrayNodeDefinition; use Symfony\Component\Config\Definition\Builder\TreeBuilder; use Symfony\Component\Config\Definition\ConfigurationInterface; @@ -48,6 +52,22 @@ private function addResourcesSection(ArrayNodeDefinition $node): void ->end() ->end() ->end() + ->arrayNode('credit_card') + ->addDefaultsIfNotSet() + ->children() + ->variableNode('options')->end() + ->arrayNode('classes') + ->addDefaultsIfNotSet() + ->children() + ->scalarNode('model')->defaultValue(CreditCard::class)->cannotBeEmpty()->end() + ->scalarNode('interface')->defaultValue(CreditCardInterface::class)->cannotBeEmpty()->end() + ->scalarNode('controller')->defaultValue(ResourceController::class)->cannotBeEmpty()->end() + ->scalarNode('factory')->defaultValue(Factory::class)->cannotBeEmpty()->end() + ->scalarNode('repository')->defaultValue(EntityRepository::class)->cannotBeEmpty()->end() + ->end() + ->end() + ->end() + ->end() ->end() ->end() ->end() diff --git a/src/Entity/CreditCard.php b/src/Entity/CreditCard.php new file mode 100644 index 00000000..cedd6757 --- /dev/null +++ b/src/Entity/CreditCard.php @@ -0,0 +1,74 @@ +id; + } + + public function getToken(): ?string + { + return $this->token; + } + + public function setToken(?string $token): void + { + $this->token = $token; + } + + public function getBrand(): ?string + { + return $this->brand; + } + + public function setBrand(?string $brand): void + { + $this->brand = $brand; + } + + public function getTail(): ?string + { + return $this->tail; + } + + public function setTail(?string $tail): void + { + $this->tail = $tail; + } + public function getExpirationDate(): ?\DateTimeInterface + { + return $this->expirationDate; + } + + public function setExpirationDate(?\DateTimeInterface $expirationDate): void + { + $this->expirationDate = $expirationDate; + } + + public function getCustomer(): ?CustomerInterface + { + return $this->customer; + } + + public function setCustomer(?CustomerInterface $customer): void + { + $this->customer = $customer; + } +} diff --git a/src/Entity/CreditCardInterface.php b/src/Entity/CreditCardInterface.php new file mode 100644 index 00000000..10c33077 --- /dev/null +++ b/src/Entity/CreditCardInterface.php @@ -0,0 +1,29 @@ +notifyTokenFactory->create($model, $gatewayName, $localeCode); - $paymentDetails = PaymentDetails::fromArray($model->getDetails()); - $this->do( fn () => $this->api->transactions()->createTransaction( $this->createCardPaymentPayloadFactory->createFrom($model, $notifyToken->getTargetUrl(), $localeCode), diff --git a/src/Payum/Action/Api/NotifyAction.php b/src/Payum/Action/Api/NotifyAction.php index 43ef6f69..15c37b02 100644 --- a/src/Payum/Action/Api/NotifyAction.php +++ b/src/Payum/Action/Api/NotifyAction.php @@ -6,6 +6,7 @@ use CommerceWeavers\SyliusTpayPlugin\Model\PaymentDetails; use CommerceWeavers\SyliusTpayPlugin\Payum\Request\Api\Notify; +use CommerceWeavers\SyliusTpayPlugin\Payum\Request\Api\SaveCreditCard; use CommerceWeavers\SyliusTpayPlugin\Tpay\Security\Notification\Factory\BasicPaymentFactoryInterface; use CommerceWeavers\SyliusTpayPlugin\Tpay\Security\Notification\Verifier\ChecksumVerifierInterface; use CommerceWeavers\SyliusTpayPlugin\Tpay\Security\Notification\Verifier\SignatureVerifierInterface; @@ -14,6 +15,7 @@ use Payum\Core\Reply\HttpResponse; use Payum\Core\Request\Generic; use Sylius\Component\Core\Model\PaymentInterface; +use Webmozart\Assert\Assert; final class NotifyAction extends BasePaymentAwareAction implements GatewayAwareInterface { @@ -51,6 +53,18 @@ protected function doExecute(Generic $request, PaymentInterface $model, PaymentD /** @var string $status */ $status = $basicPayment->tr_status; + $cardToken = $requestData->requestParameters['card_token'] ?? null; + + if ($cardToken !== null) { + $cardBrand = $requestData->requestParameters['card_brand'] ?? null; + $cardTail = $requestData->requestParameters['card_tail'] ?? null; + $tokenExpirationDate = $requestData->requestParameters['token_expiry_date'] ?? null; + + Assert::allString([$cardToken, $cardBrand, $cardTail, $tokenExpirationDate]); + + $this->gateway->execute(new SaveCreditCard($model, $cardToken, $cardBrand, $cardTail, $tokenExpirationDate)); + } + $newPaymentStatus = match (true) { str_contains($status, 'TRUE') => PaymentInterface::STATE_COMPLETED, str_contains($status, 'CHARGEBACK') => PaymentInterface::STATE_REFUNDED, diff --git a/src/Payum/Action/Api/SaveCreditCardAction.php b/src/Payum/Action/Api/SaveCreditCardAction.php new file mode 100644 index 00000000..cbf24287 --- /dev/null +++ b/src/Payum/Action/Api/SaveCreditCardAction.php @@ -0,0 +1,55 @@ +creditCardFactory->createNew(); + + $creditCard->setToken($request->cardToken); + $creditCard->setBrand($request->cardBrand); + $creditCard->setTail($request->cardTail); + $creditCard->setCustomer($model->getOrder()->getCustomer()); + $creditCard->setExpirationDate(new \DateTimeImmutable( + sprintf( + '01-%s-20%s', + substr($request->tokenExpiryDate, 0, 2), + substr($request->tokenExpiryDate, 2, 2) + ) + )); + + $this->creditCardRepository->add($creditCard); + } + + public function supports($request): bool + { + return $request instanceof SaveCreditCard && $request->getModel() instanceof PaymentInterface; + } +} diff --git a/src/Payum/Request/Api/SaveCreditCard.php b/src/Payum/Request/Api/SaveCreditCard.php new file mode 100644 index 00000000..1887f097 --- /dev/null +++ b/src/Payum/Request/Api/SaveCreditCard.php @@ -0,0 +1,21 @@ +loadFixturesFromDirectory('shop/paying_for_orders_by_card'); + + $order = $this->doPlaceOrder(tokenValue: 't0k3n', email: self::FIXTURE_EMAIL, paymentMethodCode: 'tpay_card'); + + $authorizationHeader = $this->logInUser('shop', self::FIXTURE_EMAIL); + + $this->client->request( + Request::METHOD_POST, + sprintf('/api/v2/shop/orders/%s/pay', $order->getTokenValue()), + server: self::CONTENT_TYPE_HEADER + $authorizationHeader, + content: json_encode([ + 'successUrl' => 'https://example.com/success', + 'failureUrl' => 'https://example.com/failure', + 'encodedCardData' => $this->encryptCardData( + '2223 0002 8000 0016', + new \DateTimeImmutable('2029-12-31'), + '123', + ), + 'saveCard' => true, + ]), + ); + + $response = $this->client->getResponse(); + + $this->assertResponseCode($response, Response::HTTP_OK); + $this->assertResponse($response, 'shop/paying_for_orders_by_card/test_paying_with_a_valid_card_for_an_order'); + } + + + public function test_trying_saving_cart_without_being_logged_in(): void + { + $this->loadFixturesFromDirectory('shop/paying_for_orders_by_card'); + + $order = $this->doPlaceOrder(tokenValue: 't0k3n', email: self::FIXTURE_EMAIL, paymentMethodCode: 'tpay_card'); + + $this->client->request( + Request::METHOD_POST, + sprintf('/api/v2/shop/orders/%s/pay', $order->getTokenValue()), + server: self::CONTENT_TYPE_HEADER, + content: json_encode([ + 'successUrl' => 'https://example.com/success', + 'failureUrl' => 'https://example.com/failure', + 'encodedCardData' => $this->encryptCardData( + '2223 0002 8000 0016', + new \DateTimeImmutable('2029-12-31'), + '123', + ), + 'saveCard' => true, + ]), + ); + + $response = $this->client->getResponse(); + + $this->assertResponseCode($response, 424); + $this->assertStringContainsString( + 'An error occurred while processing your payment. Please try again or contact store support.', + $response->getContent(), + ); + } + /** * @dataProvider data_provider_paying_without_a_card_data_when_a_tpay_card_payment_has_been_chosen */ diff --git a/tests/Api/Utils/UserLoginTrait.php b/tests/Api/Utils/UserLoginTrait.php new file mode 100644 index 00000000..28b04479 --- /dev/null +++ b/tests/Api/Utils/UserLoginTrait.php @@ -0,0 +1,27 @@ +get(sprintf('sylius.repository.%s_user', $userType)); + /** @var JWTTokenManagerInterface $manager */ + $manager = $this->get('lexik_jwt_authentication.jwt_manager'); + + /** @var UserInterface|null $user */ + $user = $shopUserRepository->findOneByEmail($email); + + $authorizationHeader = self::$kernel->getContainer()->getParameter('sylius.api.authorization_header'); + + return ['HTTP_' . $authorizationHeader => 'Bearer ' . $manager->create($user)]; + } +} diff --git a/tests/Application/.env.test b/tests/Application/.env.test index 0c341815..d76e599a 100644 --- a/tests/Application/.env.test +++ b/tests/Application/.env.test @@ -4,6 +4,12 @@ KERNEL_CLASS='App\Kernel' DATABASE_URL=mysql://root@127.0.0.1/cw_sylius_tpay_plugin_%kernel.environment%?serverVersion=5.7 +###> lexik/jwt-authentication-bundle ### +JWT_SECRET_KEY=%kernel.project_dir%/config/jwt/private-test.pem +JWT_PUBLIC_KEY=%kernel.project_dir%/config/jwt/public-test.pem +JWT_PASSPHRASE=ALL_THAT_IS_GOLD_DOES_NOT_GLITTER_NOT_ALL_THOSE_WHO_WANDER_ARE_LOST +###< lexik/jwt-authentication-bundle ### + ###> symfony/messenger ### # Sync transport turned for testing env for the ease of testing SYLIUS_MESSENGER_TRANSPORT_MAIN_DSN=sync:// diff --git a/tests/Application/config/jwt/private-test.pem b/tests/Application/config/jwt/private-test.pem new file mode 100644 index 00000000..b5a5246d --- /dev/null +++ b/tests/Application/config/jwt/private-test.pem @@ -0,0 +1,51 @@ +-----BEGIN RSA PRIVATE KEY----- +MIIJKAIBAAKCAgEAxuS1SudSNkjTQcP4H5SjzrdO29upko9KYZgUH6z5n+weDtIo +5tysdm7xY3nNAU9ixo7wrBvttuf7T1fDCVJjhzqX5iewaCZks7q9kYygCbvmrAoc +bx5D9EPZPH0sQQoa9gMuNou2nqWpVdTYCMAjxzVpqa2krioUzkBJzaWGDYiijv9q +KbjWvRUUoYFNOFIFXHFFDrK5ISBC155XiETKyBYhB1wZVWX1tHe1nDW609BHAAsr +1Ve3uiodzYzQ7S9Rw9Q6RCRSRgZRzFV1GTJEuyMpCCD51DA4otYeEPQf+8hvV3aK +bSNydzrQICY95kfB0p9HxorBPh8QHq0qKZOIle1Aglp0UV3OWgXLWncNc0m8e8hT +2I55lYkLio99/4PGfalAdJBPhKtTzbJllaERHOnlMkEvwk7eggkbbXEN/Ay6usi0 +R8mRaxhMkS9i8MxubgQBDsOomtegRqA1EzSGU/FJMS5g/I/gO9bjFu2l2LJwd6B4 +t/FZt/9mAIGYbIj5/Ykd0E1WIKYAIRUoyW1gTrGe70yxdHEPEILnZZMRVJzDbkVY +fgKAFUpAUqbHTtS+YI6p9tjFuxBrc6GZR/kppL/MkEARDX0ZX3n4sPLQf/vdR7+K +s8Yqws1IqsZ3h8iP8WpykbEwnd1w49A1ZBBinIXU3idf41EtQgawbpK5Oy0CAwEA +AQKCAgAEdUnvBOJd3yIFHlxocM9/KbK10OWrKFUVfPAuiZUK1aMS1/kcu6OOAAyf +GzLSLbJcGwYgBXw9llOWwrPXeKZMeK7A9PDKVNn7AVuQcKOBtFmGT6+1eesyBXdQ +GMouJwjVrNqTVGxif/oct2mkQJJMu9DDgeXoFX9j5CMDXgt0MDTcmbMKfl8p29gb +iqdtdME0AkH3A2CM8oktBhqWLlyRQZW58YGL3X41bl1+w+GNL+T3hkiUPqQaoykJ +23cvadkeV5p6vomtkiSxPNUkHHFX9IDN8tdGv1H1rHD+FkrFPQfp4PlXWu0M6R+T +KOhISiF5FCLqu197gfy9g0onpmvwTkQW0ap5kTMfmryhc2fSbGeQO1bDjnCQQ05/ +yXpu9dRvQQCbXsAIUaJUyOsgJy4tpOlmra9mFK0/+ObN7ZJDxYA1tHPKoArcSFnC +L0nbMs7C5b5Njky10nD6d1hu+HBEB1g6wBmsOCEMNBd6AF9ABmA/TIGSS2rsjdf6 +eYytYSAlSVlYwel3mvtpwfq5/q1mPtr38ND192/FDCMoVWoUbpYTKQHvAYdodOyS +DJNH6upchKdCMItv6K6rv4Rc+lN9lp/XGYlxO8BpXVO+IF/dD+6Rs5vv3Hk5lU3D +aX/ALBTTN1Xe8JKaCHd4ji1rgsIOxRmXDqdy05kiQtt7LtkCDQKCAQEA5Xw9EjXw +Wsb42j+Ew3ISAC92VBIBmSz6hnwX27vON51Bm3j86LV1GHl0PvEl0A7c46+scumG +pKgY5qad1Z7FtyCi78a9cr7HVbX03QndFZpzD14oIugN7y3ecSppPI/hfmOwfOAa +O8b2lg2A6s4QBG70SAGFVeTozxovL/V4EWly/NW/2gAe6ZAWU8c3XyJBMMhA5Ez8 +aZONHipHi+uAiOwsLth7rGZbjyF4/QpXJuKzZ8p8e43yJiCkV7F588akp13NDmIB +qXHNLEUcE1SD4GGoFGEacOkVkXO5Bqyn15LACjyluhhzsl5IWdClRUfjgbg3ieWO +wdHq3bL6TgFuBwKCAQEA3d+drcpzMUOJRrDr08yNr6ygkeIvRtX7De3ilhg0Y3G3 +L2/rexfe1i5yrSc5N5hTWjLOSUtT4AD4tIgvNwW4YRtdQ9eRr/aoFHy5w8oXm0z5 +TJta7yBgcXTc4xQE9XtVEL0OUOKXxrh+Y5HHh5do5blsNfutHqRjtKWvPp1kCHba +GjMiLkleivE4WGMkuBXPa13GUe3UfxOQp3KfaNcWAlNhnbc/8ljQJ1YZEj3XDodB +RvorGzw8m8U+yZ/rhslPiq2MD0IrKUg5/r1C8k6ZVTrcG82A3Su1gacpS4b3q84W +r1AwM3iajzBnPkiRc98H7p07vwYCeLV2Lm/w/LZAKwKCAQEAqx0HYKPNk7KXbg08 +zoso9vBs9+TxQijyqQKwu4x/CKL+f5IoatCa/mPZlPE0872RYUjlek28stwQrTOB +rv6TiKgSNl3ndz7f3X4ulf671lbzAnt/y/9iHH0ERzeLfrf+OMLWn1Zu2THTPjHV +db+u289r4KEZreFg4sQweT88hycssXAkfMXoRtnEfDWoiQw+tcQr9s+cypBWAi8e +aCtzDSWlEE0lcnhkPwaDc5KZR4p0oaivR2WhMGLYh/by6x2sOovL0bSsbo9HoIHr +nFJBfzbyIDgDgjuadHlodpyZDjoDbd6o6GlBI7f/lNDp2w3uixQ0fWMpHkaLLUI+ +N5oDUwKCAQAIjvemHIkU/WXuNCTkpp9Qh3gqKG9qbBajEuoKoCRlMZ2/VrHera0K +1f/Wbgzm+Bk/AXaznRQ/L8poLFil5rKWDFgspcQY5YrWP3lq9AC1HOMA8X0wfC88 +MSXUHJGUZo2Bd8l1lUgFglhdvuHTeSOyuNRTwMGMzQqLjViVMb0KFouTNyW6Y1oi +QevKfQiNkUnO+m8L+gCYZkjOLL25bZKLxGufidINpx9gZRHSglApX05FTqEbC9fK +qnEhlemf6WQIFWmxrPu9O+wAx4wtjJqdjweuit7NqUH3HluZbjtfhTOaz50MXzqX +C2bwIBx8O74ylh4X4EN4JIfKgsbo+J7BAoIBAAJ72s6YSoHaQCYXynLhRGenVHtI +rj6wNkwnBEsIDk9j8vt5fMJA1xRNZ0kA1mDAgDqq9ad2RpGcZH1jjvI5IPdB0dKE +5fyKR5okRMNRMc2Sn5LOiLsSqnhHwZo1nEZP/UTcZvIKDqajy8t4cIvBEO1ol4D5 +DxiclH7UMAgwKemYbsBHOOscbN2Z3o41uzSKUhNlLV5GP3ZPMau+MHinIXtCjHdi +Xu9eGA3GDD4/sU4JZTl1g/Rs48JEn2H800pVgyzkn9Q01hJZ0dSqy+Agcu3Yw6Sr +XRaqXN38pEInJ+GAU6y+6/RsiHdF3YOOUOPUX6PCfu8BMLRFASAdMbwNBXk= +-----END RSA PRIVATE KEY----- diff --git a/tests/Application/config/jwt/public-test.pem b/tests/Application/config/jwt/public-test.pem new file mode 100644 index 00000000..e21c0559 --- /dev/null +++ b/tests/Application/config/jwt/public-test.pem @@ -0,0 +1,14 @@ +-----BEGIN PUBLIC KEY----- +MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAxuS1SudSNkjTQcP4H5Sj +zrdO29upko9KYZgUH6z5n+weDtIo5tysdm7xY3nNAU9ixo7wrBvttuf7T1fDCVJj +hzqX5iewaCZks7q9kYygCbvmrAocbx5D9EPZPH0sQQoa9gMuNou2nqWpVdTYCMAj +xzVpqa2krioUzkBJzaWGDYiijv9qKbjWvRUUoYFNOFIFXHFFDrK5ISBC155XiETK +yBYhB1wZVWX1tHe1nDW609BHAAsr1Ve3uiodzYzQ7S9Rw9Q6RCRSRgZRzFV1GTJE +uyMpCCD51DA4otYeEPQf+8hvV3aKbSNydzrQICY95kfB0p9HxorBPh8QHq0qKZOI +le1Aglp0UV3OWgXLWncNc0m8e8hT2I55lYkLio99/4PGfalAdJBPhKtTzbJllaER +HOnlMkEvwk7eggkbbXEN/Ay6usi0R8mRaxhMkS9i8MxubgQBDsOomtegRqA1EzSG +U/FJMS5g/I/gO9bjFu2l2LJwd6B4t/FZt/9mAIGYbIj5/Ykd0E1WIKYAIRUoyW1g +TrGe70yxdHEPEILnZZMRVJzDbkVYfgKAFUpAUqbHTtS+YI6p9tjFuxBrc6GZR/kp +pL/MkEARDX0ZX3n4sPLQf/vdR7+Ks8Yqws1IqsZ3h8iP8WpykbEwnd1w49A1ZBBi +nIXU3idf41EtQgawbpK5Oy0CAwEAAQ== +-----END PUBLIC KEY----- diff --git a/tests/E2E/Checkout/TpayCreditCardCheckoutTest.php b/tests/E2E/Checkout/TpayCreditCardCheckoutTest.php index 82143313..85b66e8a 100644 --- a/tests/E2E/Checkout/TpayCreditCardCheckoutTest.php +++ b/tests/E2E/Checkout/TpayCreditCardCheckoutTest.php @@ -36,7 +36,7 @@ public function test_it_completes_the_checkout_using_credit_card(): void $this->loginShopUser('tony@nonexisting.cw', 'sylius'); $this->processWithPaymentMethod('tpay_card'); - $this->fillCardData(self::FORM_ID, 'John Doe', self::CARD_NUMBER, '123', '01', '2029'); + $this->fillCardData(self::FORM_ID, self::CARD_NUMBER, '123', '01', '2029'); $this->placeOrder(); $this->assertPageTitleContains('Thank you!'); @@ -47,7 +47,7 @@ public function test_it_completes_the_checkout_using_credit_card_and_saves_the_c $this->loginShopUser('tony@nonexisting.cw', 'sylius'); $this->processWithPaymentMethod('tpay_card'); - $this->fillCardData(self::FORM_ID, 'John Doe', self::CARD_NUMBER, '123', '01', '2029', true); + $this->fillCardData(self::FORM_ID, self::CARD_NUMBER, '123', '01', '2029', true); $this->placeOrder(); $this->assertPageTitleContains('Thank you!'); @@ -58,6 +58,6 @@ public function test_it_forbids_card_saving_for_not_logged_in_users(): void $this->expectException(NoSuchElementException::class); $this->processWithPaymentMethod('tpay_card'); - $this->fillCardData(self::FORM_ID, 'John Doe', self::CARD_NUMBER, '123', '01', '2029', true); + $this->fillCardData(self::FORM_ID, self::CARD_NUMBER, '123', '01', '2029', true); } } diff --git a/tests/E2E/Helper/Order/TpayTrait.php b/tests/E2E/Helper/Order/TpayTrait.php index 40398b5d..443ece71 100644 --- a/tests/E2E/Helper/Order/TpayTrait.php +++ b/tests/E2E/Helper/Order/TpayTrait.php @@ -13,7 +13,7 @@ */ trait TpayTrait { - public function fillCardData(string $formId, string $cardNumber, string $cvv, string $month, string $year, bool $saveCardForLater): void + public function fillCardData(string $formId, string $cardNumber, string $cvv, string $month, string $year, bool $saveCardForLater = false): void { $this->client->findElement(WebDriverBy::id(sprintf('%s_tpay_card_number', $formId)))->sendKeys($cardNumber); $this->client->findElement(WebDriverBy::id(sprintf('%s_tpay_card_cvv', $formId)))->sendKeys($cvv); diff --git a/tests/Unit/Api/Command/PayByCardHandlerTest.php b/tests/Unit/Api/Command/PayByCardHandlerTest.php index c978204b..9e256ed8 100644 --- a/tests/Unit/Api/Command/PayByCardHandlerTest.php +++ b/tests/Unit/Api/Command/PayByCardHandlerTest.php @@ -15,6 +15,7 @@ use Sylius\Component\Core\Repository\PaymentRepositoryInterface; use Symfony\Component\HttpKernel\Exception\NotFoundHttpException; use Tests\CommerceWeavers\SyliusTpayPlugin\Helper\PaymentDetailsHelperTrait; +use function Symfony\Component\Translation\t; final class PayByCardHandlerTest extends TestCase { @@ -60,6 +61,22 @@ public function test_it_creates_a_card_based_transaction(): void $this->assertSame('https://cw.org/pay', $result->transactionPaymentUrl); } + public function test_it_creates_a_card_based_transaction_with_saving_card_option(): void + { + $payment = $this->prophesize(PaymentInterface::class); + $payment->getDetails()->willReturn([], ['tpay' => ['status' => 'pending', 'payment_url' => 'https://cw.org/pay']]); + $payment->setDetails( + $this->getExpectedDetails(card: 'encoded_card_data', saveCreditCardForLater: true), + )->shouldBeCalled(); + + $this->paymentRepository->find(1)->willReturn($payment); + + $result = $this->createTestSubject()->__invoke(new PayByCard(1, 'encoded_card_data', true)); + + $this->assertSame('pending', $result->status); + $this->assertSame('https://cw.org/pay', $result->transactionPaymentUrl); + } + private function createTestSubject(): PayByCardHandler { return new PayByCardHandler( diff --git a/tests/Unit/Api/Factory/NextCommand/PayByCardFactoryTest.php b/tests/Unit/Api/Factory/NextCommand/PayByCardFactoryTest.php index a2ea214e..06c59671 100644 --- a/tests/Unit/Api/Factory/NextCommand/PayByCardFactoryTest.php +++ b/tests/Unit/Api/Factory/NextCommand/PayByCardFactoryTest.php @@ -47,6 +47,15 @@ public function test_it_creates_a_pay_by_card_command(): void $this->assertSame('card_data', $command->encodedCardData); } + public function test_it_creates_a_pay_by_card_command_with_save_card_information(): void + { + $command = $this->createTestSubject()->create($this->createCommand(encodedCardData: 'card_data', saveCard: true), $this->createPayment()); + + $this->assertInstanceOf(PayByCard::class, $command); + $this->assertSame('card_data', $command->encodedCardData); + $this->assertSame(true, $command->saveCard); + } + public function test_it_throws_an_exception_when_trying_to_create_a_command_with_unsupported_factory(): void { $this->expectException(UnsupportedNextCommandFactory::class); @@ -54,13 +63,14 @@ public function test_it_throws_an_exception_when_trying_to_create_a_command_with $this->createTestSubject()->create($this->createCommand(), new Payment()); } - private function createCommand(?string $token = null, ?string $encodedCardData = null): Pay + private function createCommand(?string $token = null, ?string $encodedCardData = null, bool $saveCard = false): Pay { return new Pay( $token ?? 'token', 'https://cw.nonexisting/success', 'https://cw.nonexisting/failure', encodedCardData: $encodedCardData, + saveCard: $saveCard, ); } diff --git a/tests/Unit/Payum/Action/Api/NotifyActionTest.php b/tests/Unit/Payum/Action/Api/NotifyActionTest.php index 611e53c8..4f894f37 100644 --- a/tests/Unit/Payum/Action/Api/NotifyActionTest.php +++ b/tests/Unit/Payum/Action/Api/NotifyActionTest.php @@ -7,10 +7,12 @@ use CommerceWeavers\SyliusTpayPlugin\Payum\Action\Api\NotifyAction; use CommerceWeavers\SyliusTpayPlugin\Payum\Request\Api\Notify; use CommerceWeavers\SyliusTpayPlugin\Payum\Request\Api\Notify\NotifyData; +use CommerceWeavers\SyliusTpayPlugin\Payum\Request\Api\SaveCreditCard; use CommerceWeavers\SyliusTpayPlugin\Tpay\Security\Notification\Factory\BasicPaymentFactoryInterface; use CommerceWeavers\SyliusTpayPlugin\Tpay\Security\Notification\Verifier\ChecksumVerifierInterface; use CommerceWeavers\SyliusTpayPlugin\Tpay\Security\Notification\Verifier\SignatureVerifierInterface; use CommerceWeavers\SyliusTpayPlugin\Tpay\TpayApi; +use Payum\Core\GatewayInterface; use Payum\Core\Reply\HttpResponse; use Payum\Core\Request\Sync; use Payum\Core\Security\TokenInterface; @@ -40,6 +42,8 @@ final class NotifyActionTest extends TestCase private SignatureVerifierInterface|ObjectProphecy $signatureVerifier; + private GatewayInterface|ObjectProphecy $gateway; + protected function setUp(): void { $this->request = $this->prophesize(Notify::class); @@ -48,6 +52,7 @@ protected function setUp(): void $this->basicPaymentFactory = $this->prophesize(BasicPaymentFactoryInterface::class); $this->checksumVerifier = $this->prophesize(ChecksumVerifierInterface::class); $this->signatureVerifier = $this->prophesize(SignatureVerifierInterface::class); + $this->gateway = $this->prophesize(GatewayInterface::class); $order = $this->prophesize(OrderInterface::class); $order->getLocaleCode()->willReturn('en_US'); @@ -81,19 +86,32 @@ public function test_it_supports_only_payment_interface_based_models(): void /** * @dataProvider data_provider_it_converts_tpay_notification_status */ - public function test_it_converts_tpay_notification_status(string $status, string $expectedStatus): void + public function test_it_converts_tpay_notification_status(string $status, string $expectedStatus, bool $isSavingCard): void { + $requestParameters = [ + 'tr_status' => $status, + ]; + + if ($isSavingCard) { + $requestParameters['card_token'] = '2c1f4fa4389a34071fc98ac1382ef30efb35c6dbe600415e6c29618fc57cfc02'; + $requestParameters['card_brand'] = '0016'; + $requestParameters['card_tail'] = 'Mastercard'; + $requestParameters['token_expiry_date'] = '0128'; + + $this->gateway->execute( + new SaveCreditCard($this->model->reveal(), '2c1f4fa4389a34071fc98ac1382ef30efb35c6dbe600415e6c29618fc57cfc02', '0016', 'Mastercard', '0128') + )->shouldBeCalled(); + } + $this->request->getData()->willReturn(new NotifyData( 'jws', 'content', - [ - 'tr_status' => $status, - ], + $requestParameters, )); $this->api->getNotificationSecretCode()->willReturn('merchant_code'); - $this->basicPaymentFactory->createFromArray(['tr_status' => $status])->willReturn($basicPayment = new BasicPayment()); + $this->basicPaymentFactory->createFromArray($requestParameters)->willReturn($basicPayment = new BasicPayment()); $basicPayment->tr_status = $status; $this->checksumVerifier->verify($basicPayment, 'merchant_code')->willReturn(true); @@ -165,9 +183,10 @@ public function test_it_throws_false_http_reply_when_signature_is_invalid(): voi public static function data_provider_it_converts_tpay_notification_status(): iterable { - yield 'status containing the `TRUE` word' => ['TRUE', PaymentInterface::STATE_COMPLETED]; - yield 'status containing the other than `TRUE` word' => ['FALSE', PaymentInterface::STATE_FAILED]; - yield 'status containing the `CHARGEBACK` word' => ['CHARGEBACK', PaymentInterface::STATE_REFUNDED]; + yield 'status containing the `TRUE` word' => ['TRUE', PaymentInterface::STATE_COMPLETED, false]; + yield 'status containing the `TRUE` word and saving card' => ['TRUE', PaymentInterface::STATE_COMPLETED, true]; + yield 'status containing the other than `TRUE` word' => ['FALSE', PaymentInterface::STATE_FAILED, false]; + yield 'status containing the `CHARGEBACK` word' => ['CHARGEBACK', PaymentInterface::STATE_REFUNDED, false]; } private function createNotifyDataObject(string $jws = 'jws', string $content = 'content', array $parameters = []): NotifyData @@ -184,6 +203,7 @@ private function createTestSubject(): NotifyAction ); $action->setApi($this->api->reveal()); + $action->setGateway($this->gateway->reveal()); return $action; } diff --git a/tests/Unit/Payum/Action/Api/SaveCreditCardActionTest.php b/tests/Unit/Payum/Action/Api/SaveCreditCardActionTest.php new file mode 100644 index 00000000..b9059ffb --- /dev/null +++ b/tests/Unit/Payum/Action/Api/SaveCreditCardActionTest.php @@ -0,0 +1,112 @@ +model = $this->prophesize(PaymentInterface::class); + $this->api = $this->prophesize(TpayApi::class); + $this->factory = $this->prophesize(FactoryInterface::class); + $this->repository = $this->prophesize(RepositoryInterface::class); + + $order = $this->prophesize(OrderInterface::class); + $order->getLocaleCode()->willReturn('en_US'); + + $this->customer = $this->prophesize(CustomerInterface::class); + $order->getCustomer()->willReturn($this->customer->reveal()); + + $this->model = $this->prophesize(PaymentInterface::class); + $this->model->getOrder()->willReturn($order->reveal()); + $this->model->getDetails()->willReturn([]); + + $token = $this->prophesize(TokenInterface::class); + $token->getGatewayName()->willReturn('tpay'); + + $this->request = new SaveCreditCard($token->reveal(), 'card_token', 'card_brand', 'card_tail', '1128'); + $this->request->setModel($this->model->reveal()); + } + + public function test_it_supports_only_save_credit_card_requests(): void + { + $action = $this->createTestSubject(); + + $this->assertFalse($action->supports(new Sync($this->model->reveal()))); + $this->assertTrue($action->supports($this->request)); + } + + public function test_it_supports_only_payment_interface_based_models(): void + { + $action = $this->createTestSubject(); + + $this->assertFalse($action->supports(new SaveCreditCard(new \stdClass(), 'card_token', 'card_brand', 'card_tail', 'token_expiry_date'))); + $this->assertTrue($action->supports($this->request)); + } + + public function test_it_saves_returned_credit_card(): void + { + $creditCard = $this->prophesize(CreditCardInterface::class); + $this->factory->createNew()->willReturn($creditCard->reveal()); + + $this->model->setDetails($this->getExpectedDetails())->shouldBeCalled(); + + $creditCard->setTail('card_tail')->shouldBeCalled(); + $creditCard->setBrand('card_brand')->shouldBeCalled(); + $creditCard->setToken('card_token')->shouldBeCalled(); + $creditCard->setExpirationDate(new \DateTimeImmutable('01-11-2028'))->shouldBeCalled(); + $creditCard->setCustomer($this->customer)->shouldBeCalled(); + + $this->repository->add($creditCard->reveal())->shouldBeCalled(); + + $this->createTestSubject()->execute($this->request); + } + + private function createTestSubject(): SaveCreditCardAction + { + $action = new SaveCreditCardAction( + $this->factory->reveal(), + $this->repository->reveal(), + ); + + $action->setApi($this->api->reveal()); + + return $action; + } +} diff --git a/tests/mockoon_tpay.json b/tests/mockoon_tpay.json index 6430147d..9aa600fc 100644 --- a/tests/mockoon_tpay.json +++ b/tests/mockoon_tpay.json @@ -841,4 +841,4 @@ ], "data": [], "callbacks": [] -} \ No newline at end of file +} From 7334ef9db615cd9fa7a7cfcce787d13a7c419d0f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=81ukasz=20Chru=C5=9Bciel?= Date: Tue, 29 Oct 2024 19:02:30 +0100 Subject: [PATCH 05/20] Add support for credit card CRUD via API --- config/api_resources/credit_card.yaml | 12 +++++ config/services/api/doctrine.php | 16 ++++++ .../CreditCardShopUserCollectionExtension.php | 48 +++++++++++++++++ .../CreditCardShopUserItemExtension.php | 52 +++++++++++++++++++ 4 files changed, 128 insertions(+) create mode 100644 config/api_resources/credit_card.yaml create mode 100644 src/Api/Doctrine/QueryCollectionExtension/CreditCardShopUserCollectionExtension.php create mode 100644 src/Api/Doctrine/QueryItemExtension/CreditCardShopUserItemExtension.php diff --git a/config/api_resources/credit_card.yaml b/config/api_resources/credit_card.yaml new file mode 100644 index 00000000..212d4caa --- /dev/null +++ b/config/api_resources/credit_card.yaml @@ -0,0 +1,12 @@ +'%commerce_weavers_sylius_tpay.model.credit_card.class%': + collectionOperations: + get: + path: /shop/credit-cards + itemOperations: + get: + path: /shop/credit-cards/{id} + delete: + path: /shop/credit-cards/{id} + properties: + id: + identifier: true diff --git a/config/services/api/doctrine.php b/config/services/api/doctrine.php index 5d4b2d7e..4374fcfc 100644 --- a/config/services/api/doctrine.php +++ b/config/services/api/doctrine.php @@ -4,6 +4,8 @@ namespace Symfony\Component\DependencyInjection\Loader\Configurator; +use CommerceWeavers\SyliusTpayPlugin\Api\Doctrine\QueryCollectionExtension\CreditCardShopUserCollectionExtension; +use CommerceWeavers\SyliusTpayPlugin\Api\Doctrine\QueryItemExtension\CreditCardShopUserItemExtension; use CommerceWeavers\SyliusTpayPlugin\Api\Doctrine\QueryItemExtension\OrderShopUserItemExtension; use CommerceWeavers\SyliusTpayPlugin\Api\Doctrine\QueryItemExtension\OrderVisitorItemExtension; use CommerceWeavers\SyliusTpayPlugin\Api\Doctrine\QueryItemExtension\Provider\AllowedOrderOperationsProvider; @@ -25,6 +27,20 @@ ]) ; + $services->set('commerce_weavers_sylius_tpay.api.doctrine.query_item_extension.credit_card_shop_user', CreditCardShopUserItemExtension::class) + ->args([ + service(UserContextInterface::class), + ]) + ->tag('api_platform.doctrine.orm.query_extension.item') + ; + + $services->set('commerce_weavers_sylius_tpay.api.doctrine.query_collection_extension.credit_card_shop_user', CreditCardShopUserCollectionExtension::class) + ->args([ + service(UserContextInterface::class), + ]) + ->tag('api_platform.doctrine.orm.query_extension.collection') + ; + $services->set('commerce_weavers_sylius_tpay.api.doctrine.query_item_extension.order_visitor', OrderVisitorItemExtension::class) ->decorate(\Sylius\Bundle\ApiBundle\Doctrine\QueryItemExtension\OrderVisitorItemExtension::class) ->args([ diff --git a/src/Api/Doctrine/QueryCollectionExtension/CreditCardShopUserCollectionExtension.php b/src/Api/Doctrine/QueryCollectionExtension/CreditCardShopUserCollectionExtension.php new file mode 100644 index 00000000..42d2663e --- /dev/null +++ b/src/Api/Doctrine/QueryCollectionExtension/CreditCardShopUserCollectionExtension.php @@ -0,0 +1,48 @@ + $context */ + public function applyToCollection( + QueryBuilder $queryBuilder, + LegacyQueryNameGeneratorInterface $queryNameGenerator, + string $resourceClass, + ?string $operationName = null, + array $context = [], + ): void { + if (!is_a($resourceClass, CreditCardInterface::class, true)) { + return; + } + + $user = $this->userContext->getUser(); + + if ($user instanceof AdminUserInterface) { + return; + } + + $customer = $user?->getCustomer(); + + $rootAlias = $queryBuilder->getRootAliases()[0]; + $customerParameterName = $queryNameGenerator->generateParameterName('customer'); + + $queryBuilder + ->andWhere(sprintf('%s.customer = :%s', $rootAlias, $customerParameterName)) + ->setParameter($customerParameterName, $customer) + ; + } +} diff --git a/src/Api/Doctrine/QueryItemExtension/CreditCardShopUserItemExtension.php b/src/Api/Doctrine/QueryItemExtension/CreditCardShopUserItemExtension.php new file mode 100644 index 00000000..ead7309b --- /dev/null +++ b/src/Api/Doctrine/QueryItemExtension/CreditCardShopUserItemExtension.php @@ -0,0 +1,52 @@ +userContext->getUser(); + + if ($user instanceof AdminUserInterface) { + return; + } + + $customer = $user?->getCustomer(); + + $rootAlias = $queryBuilder->getRootAliases()[0]; + $customerParameterName = $queryNameGenerator->generateParameterName('customer'); + + $queryBuilder + ->andWhere(sprintf('%s.customer = :%s', $rootAlias, $customerParameterName)) + ->setParameter($customerParameterName, $customer?->getId()) + ; + } +} From 33b68f92d0388b317d7081a34cf7b04936c910e4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=81ukasz=20Chru=C5=9Bciel?= Date: Tue, 29 Oct 2024 23:37:49 +0100 Subject: [PATCH 06/20] Add support for credit card CRUD via UI --- config/config.php | 2 + config/config/sylius_template_events.php | 7 ++++ config/grid/credit_card.yml | 38 +++++++++++++++++++ config/routes_shop.php | 29 +++++++++++++- config/services/event_listener.php | 19 ++++++++++ .../CreditCardShopUserItemExtension.php | 1 - src/DependencyInjection/Configuration.php | 3 +- ...ddCreditCardToAccountMenuEventListener.php | 22 +++++++++++ src/Repository/CreditCardRepository.php | 29 ++++++++++++++ src/Routing.php | 8 ++++ .../account/credit_card/breadcrumb.html.twig | 7 ++++ .../shop/account/credit_card/index.html.twig | 11 ++++++ .../credit_card/index/_subcontent.html.twig | 1 + translations/messages.en.yaml | 5 +++ 14 files changed, 178 insertions(+), 4 deletions(-) create mode 100644 config/grid/credit_card.yml create mode 100644 config/services/event_listener.php create mode 100644 src/EventListener/AddCreditCardToAccountMenuEventListener.php create mode 100644 src/Repository/CreditCardRepository.php create mode 100644 templates/shop/account/credit_card/breadcrumb.html.twig create mode 100644 templates/shop/account/credit_card/index.html.twig create mode 100644 templates/shop/account/credit_card/index/_subcontent.html.twig diff --git a/config/config.php b/config/config.php index a07efc0a..1cdfe31c 100644 --- a/config/config.php +++ b/config/config.php @@ -8,6 +8,8 @@ return static function(ContainerConfigurator $container): void { $container->import('config/**/*.php'); + $container->import('grid/*.yml'); + $container->import('grid/*.yaml'); $parameters = $container->parameters(); $parameters diff --git a/config/config/sylius_template_events.php b/config/config/sylius_template_events.php index 29136643..8cf5a0ac 100644 --- a/config/config/sylius_template_events.php +++ b/config/config/sylius_template_events.php @@ -75,6 +75,13 @@ ], ], ], + 'cw.tpay.shop.account.credit_card.index.subcontent' => [ + 'blocks' => [ + 'commerce_weavers_sylius_tpay_scripts' => [ + 'template' => '@CommerceWeaversSyliusTpayPlugin/shop/account/credit_card/index/_subcontent.html.twig', + ], + ], + ], 'sylius.shop.layout.javascripts' => [ 'blocks' => [ 'commerce_weavers_sylius_tpay_scripts' => [ diff --git a/config/grid/credit_card.yml b/config/grid/credit_card.yml new file mode 100644 index 00000000..5fbc3ee9 --- /dev/null +++ b/config/grid/credit_card.yml @@ -0,0 +1,38 @@ +sylius_grid: + grids: + commerce_weavers_sylius_tpay_shop_account_credit_card: + driver: + name: doctrine/orm + options: + class: "%commerce_weavers_sylius_tpay.model.credit_card.class%" + repository: + method: createByCustomerListQueryBuilder + arguments: + - "expr:service('sylius.context.customer').getCustomer().getId()" + sorting: + expirationDate: desc + fields: + brand: + type: string + label: commerce_weavers_sylius_tpay.shop.credit_card.brand + sortable: ~ + tail: + type: string + label: commerce_weavers_sylius_tpay.shop.credit_card.tail + sortable: ~ + expirationDate: + type: datetime + label: commerce_weavers_sylius_tpay.shop.credit_card.expiration_date + sortable: ~ + options: + format: m-Y + actions: + item: + delete: + type: delete + label: sylius.ui.delete + options: + link: + route: !php/const CommerceWeavers\SyliusTpayPlugin\Routing::SHOP_ACCOUNT_CREDIT_CARD_DELETE + parameters: + id: resource.id diff --git a/config/routes_shop.php b/config/routes_shop.php index 61b6f1a5..01aba0fd 100644 --- a/config/routes_shop.php +++ b/config/routes_shop.php @@ -27,8 +27,33 @@ ->methods([Request::METHOD_GET]) ; - $routes->add(Routing::SHOP_WAITING_FOR_PAYMENT, Routing::SHOP_WAITING_FOR_PAYMENT_PATH) - ->controller(DisplayWaitingForPaymentPage::class) + $routes->add(Routing::SHOP_ACCOUNT_CREDIT_CARD_INDEX, Routing::SHOP_ACCOUNT_CREDIT_CARD_INDEX_PATH) + ->controller('commerce_weavers_sylius_tpay.controller.credit_card::indexAction') ->methods([Request::METHOD_GET]) + ->defaults([ + '_sylius' => [ + 'template' => '@CommerceWeaversSyliusTpayPlugin/shop/account/credit_card/index.html.twig', + 'section' => 'shop_account', + 'grid' => 'commerce_weavers_sylius_tpay_shop_account_credit_card', + ] + ]) + ; + + $routes->add(Routing::SHOP_ACCOUNT_CREDIT_CARD_DELETE, Routing::SHOP_ACCOUNT_CREDIT_CARD_DELETE_PATH) + ->controller('commerce_weavers_sylius_tpay.controller.credit_card::deleteAction') + ->methods([Request::METHOD_DELETE]) + ->defaults([ + '_sylius' => [ + 'section' => 'shop_account', + 'repository' => [ + 'method' => 'findOneByCustomer', + 'arguments' => [ + '$id', + 'expr:service(\'sylius.context.customer\').getCustomer()', + ], + ], + 'redirect' => Routing::SHOP_ACCOUNT_CREDIT_CARD_INDEX, + ] + ]) ; }; diff --git a/config/services/event_listener.php b/config/services/event_listener.php new file mode 100644 index 00000000..316f3607 --- /dev/null +++ b/config/services/event_listener.php @@ -0,0 +1,19 @@ +services(); + + $services->set(AddCreditCardToAccountMenuEventListener::class) + ->tag('kernel.event_listener', ['event' => AccountMenuBuilder::EVENT_NAME, 'method' => '__invoke']) + + ; +}; diff --git a/src/Api/Doctrine/QueryItemExtension/CreditCardShopUserItemExtension.php b/src/Api/Doctrine/QueryItemExtension/CreditCardShopUserItemExtension.php index ead7309b..9928d962 100644 --- a/src/Api/Doctrine/QueryItemExtension/CreditCardShopUserItemExtension.php +++ b/src/Api/Doctrine/QueryItemExtension/CreditCardShopUserItemExtension.php @@ -27,7 +27,6 @@ public function applyToItem( string $operationName = null, array $context = [], ): void { - VarDumper::dump($resourceClass); if (!is_a($resourceClass, CreditCardInterface::class, true)) { return; } diff --git a/src/DependencyInjection/Configuration.php b/src/DependencyInjection/Configuration.php index 2bd3d368..ad9ce81e 100644 --- a/src/DependencyInjection/Configuration.php +++ b/src/DependencyInjection/Configuration.php @@ -10,6 +10,7 @@ use CommerceWeavers\SyliusTpayPlugin\Entity\CreditCardInterface; use CommerceWeavers\SyliusTpayPlugin\Factory\BlikAliasFactory; use CommerceWeavers\SyliusTpayPlugin\Repository\BlikAliasRepository; +use CommerceWeavers\SyliusTpayPlugin\Repository\CreditCardRepository; use Sylius\Bundle\ResourceBundle\Controller\ResourceController; use Sylius\Bundle\ResourceBundle\Doctrine\ORM\EntityRepository; use Sylius\Resource\Factory\Factory; @@ -63,7 +64,7 @@ private function addResourcesSection(ArrayNodeDefinition $node): void ->scalarNode('interface')->defaultValue(CreditCardInterface::class)->cannotBeEmpty()->end() ->scalarNode('controller')->defaultValue(ResourceController::class)->cannotBeEmpty()->end() ->scalarNode('factory')->defaultValue(Factory::class)->cannotBeEmpty()->end() - ->scalarNode('repository')->defaultValue(EntityRepository::class)->cannotBeEmpty()->end() + ->scalarNode('repository')->defaultValue(CreditCardRepository::class)->cannotBeEmpty()->end() ->end() ->end() ->end() diff --git a/src/EventListener/AddCreditCardToAccountMenuEventListener.php b/src/EventListener/AddCreditCardToAccountMenuEventListener.php new file mode 100644 index 00000000..fe964cfa --- /dev/null +++ b/src/EventListener/AddCreditCardToAccountMenuEventListener.php @@ -0,0 +1,22 @@ +getMenu(); + + $menu + ->addChild('credit_cards', ['route' => Routing::SHOP_ACCOUNT_CREDIT_CARD_INDEX]) + ->setLabel('commerce_weavers_sylius_tpay.shop.credit_cards') + ->setLabelAttribute('icon', 'credit card') + ; + } +} diff --git a/src/Repository/CreditCardRepository.php b/src/Repository/CreditCardRepository.php new file mode 100644 index 00000000..cc660deb --- /dev/null +++ b/src/Repository/CreditCardRepository.php @@ -0,0 +1,29 @@ +createQueryBuilder('o') + ->andWhere('o.customer = :customer') + ->setParameter('customer', $customerId) + ; + } + + public function findOneByCustomer(mixed $customerId): ?CreditCardInterface + { + /** @phpstan-var CreditCardInterface|null */ + return $this->createByCustomerListQueryBuilder($customerId) + ->getQuery() + ->getOneOrNullResult() + ; + } +} diff --git a/src/Routing.php b/src/Routing.php index fe2671ab..5875d53b 100644 --- a/src/Routing.php +++ b/src/Routing.php @@ -33,4 +33,12 @@ final class Routing public const SHOP_WAITING_FOR_PAYMENT = 'commerce_weavers_sylius_tpay_waiting_for_payment'; public const SHOP_WAITING_FOR_PAYMENT_PATH = '/tpay/waiting-for-payment'; + + public const SHOP_ACCOUNT_CREDIT_CARD_INDEX = 'commerce_weavers_sylius_tpay_shop_account_credit_card_index'; + + public const SHOP_ACCOUNT_CREDIT_CARD_INDEX_PATH = '/account/credit-cards'; + + public const SHOP_ACCOUNT_CREDIT_CARD_DELETE = 'commerce_weavers_sylius_tpay_shop_account_credit_card_delete'; + + public const SHOP_ACCOUNT_CREDIT_CARD_DELETE_PATH = '/account/credit-cards/{id}/delete'; } diff --git a/templates/shop/account/credit_card/breadcrumb.html.twig b/templates/shop/account/credit_card/breadcrumb.html.twig new file mode 100644 index 00000000..8ba1204f --- /dev/null +++ b/templates/shop/account/credit_card/breadcrumb.html.twig @@ -0,0 +1,7 @@ + diff --git a/templates/shop/account/credit_card/index.html.twig b/templates/shop/account/credit_card/index.html.twig new file mode 100644 index 00000000..7260b15f --- /dev/null +++ b/templates/shop/account/credit_card/index.html.twig @@ -0,0 +1,11 @@ +{% extends '@SyliusShop/Account/layout.html.twig' %} + +{% block title %}{{ 'commerce_weavers_sylius_tpay.shop.credit_cards'|trans }} | {{ parent() }}{% endblock %} + +{% block breadcrumb %} + {% include '@CommerceWeaversSyliusTpayPlugin/shop/account/credit_card/breadcrumb.html.twig' %} +{% endblock %} + +{% block subcontent %} + {{ sylius_template_event('cw.tpay.shop.account.credit_card.index.subcontent', _context) }} +{% endblock %} diff --git a/templates/shop/account/credit_card/index/_subcontent.html.twig b/templates/shop/account/credit_card/index/_subcontent.html.twig new file mode 100644 index 00000000..4a04f6ec --- /dev/null +++ b/templates/shop/account/credit_card/index/_subcontent.html.twig @@ -0,0 +1 @@ +{{ sylius_grid_render(resources, '@SyliusShop/Grid/_default.html.twig') }} diff --git a/translations/messages.en.yaml b/translations/messages.en.yaml index ee7dbd32..ce495616 100644 --- a/translations/messages.en.yaml +++ b/translations/messages.en.yaml @@ -19,6 +19,11 @@ commerce_weavers_sylius_tpay: redirect: 'Redirect to Tpay' pay_by_link: 'Redirect to bank' shop: + credit_cards: 'Credit cards' + credit_card: + brand: 'Brand' + expiration_date: 'Expiration date' + tail: 'Tail' order_summary: blik: token: 'BLIK token' From 31c249ef07669ee838cd78cc1e28ee441eb5b961 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=81ukasz=20Chru=C5=9Bciel?= Date: Wed, 30 Oct 2024 00:02:03 +0100 Subject: [PATCH 07/20] Add support for credit card channels in UI and API --- config/doctrine/CreditCard.orm.xml | 4 ++++ config/grid/credit_card.yml | 3 ++- config/routes_shop.php | 5 +++-- config/services/api/doctrine.php | 3 +++ migrations/Version20241029160137.php | 6 ++++++ .../CreditCardShopUserCollectionExtension.php | 12 ++++++++++-- .../CreditCardShopUserItemExtension.php | 14 +++++++++++--- src/Entity/CreditCard.php | 13 +++++++++++++ src/Repository/CreditCardRepository.php | 18 +++++++++++++----- .../CreditCardRepositoryInterface.php | 15 +++++++++++++++ 10 files changed, 80 insertions(+), 13 deletions(-) create mode 100644 src/Repository/CreditCardRepositoryInterface.php diff --git a/config/doctrine/CreditCard.orm.xml b/config/doctrine/CreditCard.orm.xml index 65b71251..6edca3ba 100644 --- a/config/doctrine/CreditCard.orm.xml +++ b/config/doctrine/CreditCard.orm.xml @@ -19,5 +19,9 @@ + + + + diff --git a/config/grid/credit_card.yml b/config/grid/credit_card.yml index 5fbc3ee9..3ebbf17e 100644 --- a/config/grid/credit_card.yml +++ b/config/grid/credit_card.yml @@ -8,7 +8,8 @@ sylius_grid: repository: method: createByCustomerListQueryBuilder arguments: - - "expr:service('sylius.context.customer').getCustomer().getId()" + - "expr:service('sylius.context.customer').getCustomer()" + - "expr:service('sylius.context.channel').getChannel()" sorting: expirationDate: desc fields: diff --git a/config/routes_shop.php b/config/routes_shop.php index 01aba0fd..dc7fcd61 100644 --- a/config/routes_shop.php +++ b/config/routes_shop.php @@ -46,10 +46,11 @@ '_sylius' => [ 'section' => 'shop_account', 'repository' => [ - 'method' => 'findOneByCustomer', + 'method' => 'findOneByChannelAndCustomer', 'arguments' => [ '$id', - 'expr:service(\'sylius.context.customer\').getCustomer()', + 'expr:service(\'sylius.context.customer\').getCustomer().getId()', + 'expr:service(\'sylius.context.channel\').getChannel().getId()', ], ], 'redirect' => Routing::SHOP_ACCOUNT_CREDIT_CARD_INDEX, diff --git a/config/services/api/doctrine.php b/config/services/api/doctrine.php index 4374fcfc..253e7b33 100644 --- a/config/services/api/doctrine.php +++ b/config/services/api/doctrine.php @@ -11,6 +11,7 @@ use CommerceWeavers\SyliusTpayPlugin\Api\Doctrine\QueryItemExtension\Provider\AllowedOrderOperationsProvider; use CommerceWeavers\SyliusTpayPlugin\Api\Doctrine\QueryItemExtension\Provider\AllowedOrderOperationsProviderInterface; use Sylius\Bundle\ApiBundle\Context\UserContextInterface; +use Sylius\Component\Channel\Context\ChannelContextInterface; return function(ContainerConfigurator $container): void { $services = $container->services(); @@ -30,6 +31,7 @@ $services->set('commerce_weavers_sylius_tpay.api.doctrine.query_item_extension.credit_card_shop_user', CreditCardShopUserItemExtension::class) ->args([ service(UserContextInterface::class), + service(ChannelContextInterface::class), ]) ->tag('api_platform.doctrine.orm.query_extension.item') ; @@ -37,6 +39,7 @@ $services->set('commerce_weavers_sylius_tpay.api.doctrine.query_collection_extension.credit_card_shop_user', CreditCardShopUserCollectionExtension::class) ->args([ service(UserContextInterface::class), + service(ChannelContextInterface::class), ]) ->tag('api_platform.doctrine.orm.query_extension.collection') ; diff --git a/migrations/Version20241029160137.php b/migrations/Version20241029160137.php index e7cd96ea..c0432658 100644 --- a/migrations/Version20241029160137.php +++ b/migrations/Version20241029160137.php @@ -18,11 +18,17 @@ public function up(Schema $schema): void { $this->addSql('CREATE TABLE cw_sylius_tpay_credt_card (id INT AUTO_INCREMENT NOT NULL, customer_id INT NOT NULL, token VARCHAR(255) NOT NULL, brand VARCHAR(255) NOT NULL, tail VARCHAR(255) NOT NULL, expiration_date DATETIME DEFAULT NULL, INDEX IDX_9FF1996C9395C3F3 (customer_id), PRIMARY KEY(id)) DEFAULT CHARACTER SET UTF8 COLLATE `UTF8_unicode_ci` ENGINE = InnoDB'); $this->addSql('ALTER TABLE cw_sylius_tpay_credt_card ADD CONSTRAINT FK_9FF1996C9395C3F3 FOREIGN KEY (customer_id) REFERENCES sylius_customer (id) ON DELETE CASCADE'); + $this->addSql('ALTER TABLE cw_sylius_tpay_credt_card ADD channel_id INT NOT NULL'); + $this->addSql('ALTER TABLE cw_sylius_tpay_credt_card ADD CONSTRAINT FK_9FF1996C72F5A1AA FOREIGN KEY (channel_id) REFERENCES sylius_channel (id) ON DELETE CASCADE'); + $this->addSql('CREATE INDEX IDX_9FF1996C72F5A1AA ON cw_sylius_tpay_credt_card (channel_id)'); } public function down(Schema $schema): void { $this->addSql('ALTER TABLE cw_sylius_tpay_credt_card DROP FOREIGN KEY FK_9FF1996C9395C3F3'); $this->addSql('DROP TABLE cw_sylius_tpay_credt_card'); + $this->addSql('ALTER TABLE cw_sylius_tpay_credt_card DROP FOREIGN KEY FK_9FF1996C72F5A1AA'); + $this->addSql('DROP INDEX IDX_9FF1996C72F5A1AA ON cw_sylius_tpay_credt_card'); + $this->addSql('ALTER TABLE cw_sylius_tpay_credt_card DROP channel_id'); } } diff --git a/src/Api/Doctrine/QueryCollectionExtension/CreditCardShopUserCollectionExtension.php b/src/Api/Doctrine/QueryCollectionExtension/CreditCardShopUserCollectionExtension.php index 42d2663e..d934b5c1 100644 --- a/src/Api/Doctrine/QueryCollectionExtension/CreditCardShopUserCollectionExtension.php +++ b/src/Api/Doctrine/QueryCollectionExtension/CreditCardShopUserCollectionExtension.php @@ -9,12 +9,15 @@ use CommerceWeavers\SyliusTpayPlugin\Entity\CreditCardInterface; use Doctrine\ORM\QueryBuilder; use Sylius\Bundle\ApiBundle\Context\UserContextInterface; +use Sylius\Component\Channel\Context\ChannelContextInterface; use Sylius\Component\Core\Model\AdminUserInterface; final class CreditCardShopUserCollectionExtension implements ContextAwareQueryCollectionExtensionInterface { - public function __construct(private readonly UserContextInterface $userContext) - { + public function __construct( + private readonly UserContextInterface $userContext, + private readonly ChannelContextInterface $channelContext, + ) { } /** @param array $context */ @@ -35,14 +38,19 @@ public function applyToCollection( return; } + $channel = $this->channelContext->getChannel(); + $customer = $user?->getCustomer(); $rootAlias = $queryBuilder->getRootAliases()[0]; $customerParameterName = $queryNameGenerator->generateParameterName('customer'); + $channelParameterName = $queryNameGenerator->generateParameterName('channel'); $queryBuilder ->andWhere(sprintf('%s.customer = :%s', $rootAlias, $customerParameterName)) + ->andWhere(sprintf('%s.channel = :%s', $rootAlias, $channelParameterName)) ->setParameter($customerParameterName, $customer) + ->setParameter($channelParameterName, $channel) ; } } diff --git a/src/Api/Doctrine/QueryItemExtension/CreditCardShopUserItemExtension.php b/src/Api/Doctrine/QueryItemExtension/CreditCardShopUserItemExtension.php index 9928d962..970a31a9 100644 --- a/src/Api/Doctrine/QueryItemExtension/CreditCardShopUserItemExtension.php +++ b/src/Api/Doctrine/QueryItemExtension/CreditCardShopUserItemExtension.php @@ -9,14 +9,17 @@ use CommerceWeavers\SyliusTpayPlugin\Entity\CreditCardInterface; use Doctrine\ORM\QueryBuilder; use Sylius\Bundle\ApiBundle\Context\UserContextInterface; +use Sylius\Component\Channel\Context\ChannelContextInterface; use Sylius\Component\Core\Model\AdminUserInterface; use Sylius\Component\Core\Model\ShopUserInterface; use Symfony\Component\VarDumper\VarDumper; final class CreditCardShopUserItemExtension implements QueryItemExtensionInterface { - public function __construct(private readonly UserContextInterface $userContext) - { + public function __construct( + private readonly UserContextInterface $userContext, + private readonly ChannelContextInterface $channelContext, + ) { } public function applyToItem( @@ -38,14 +41,19 @@ public function applyToItem( return; } + $channel = $this->channelContext->getChannel(); + $customer = $user?->getCustomer(); $rootAlias = $queryBuilder->getRootAliases()[0]; $customerParameterName = $queryNameGenerator->generateParameterName('customer'); + $channelParameterName = $queryNameGenerator->generateParameterName('channel'); $queryBuilder ->andWhere(sprintf('%s.customer = :%s', $rootAlias, $customerParameterName)) - ->setParameter($customerParameterName, $customer?->getId()) + ->andWhere(sprintf('%s.channel = :%s', $rootAlias, $channelParameterName)) + ->setParameter($customerParameterName, $customer) + ->setParameter($channelParameterName, $channel) ; } } diff --git a/src/Entity/CreditCard.php b/src/Entity/CreditCard.php index cedd6757..59cdf7d0 100644 --- a/src/Entity/CreditCard.php +++ b/src/Entity/CreditCard.php @@ -4,6 +4,7 @@ namespace CommerceWeavers\SyliusTpayPlugin\Entity; +use Sylius\Component\Core\Model\ChannelInterface; use Sylius\Component\Core\Model\CustomerInterface; class CreditCard implements CreditCardInterface @@ -18,6 +19,8 @@ class CreditCard implements CreditCardInterface private ?CustomerInterface $customer = null; + private ?ChannelInterface $channel = null; + public function getId(): ?int { return $this->id; @@ -71,4 +74,14 @@ public function setCustomer(?CustomerInterface $customer): void { $this->customer = $customer; } + + public function getChannel(): ?ChannelInterface + { + return $this->channel; + } + + public function setChannel(?ChannelInterface $channel): void + { + $this->channel = $channel; + } } diff --git a/src/Repository/CreditCardRepository.php b/src/Repository/CreditCardRepository.php index cc660deb..0d20a715 100644 --- a/src/Repository/CreditCardRepository.php +++ b/src/Repository/CreditCardRepository.php @@ -7,21 +7,29 @@ use CommerceWeavers\SyliusTpayPlugin\Entity\CreditCardInterface; use Doctrine\ORM\QueryBuilder; use Sylius\Bundle\ResourceBundle\Doctrine\ORM\EntityRepository; +use Sylius\Component\Core\Model\ChannelInterface; +use Sylius\Component\Core\Model\CustomerInterface; -final class CreditCardRepository extends EntityRepository +final class CreditCardRepository extends EntityRepository implements CreditCardRepositoryInterface { - public function createByCustomerListQueryBuilder(mixed $customerId): QueryBuilder + public function createByCustomerListQueryBuilder(CustomerInterface $customer, ChannelInterface $channel): QueryBuilder { return $this->createQueryBuilder('o') ->andWhere('o.customer = :customer') - ->setParameter('customer', $customerId) + ->andWhere('o.channel = :channel') + ->setParameter('customer', $customer) + ->setParameter('channel', $channel) ; } - public function findOneByCustomer(mixed $customerId): ?CreditCardInterface + public function findOneByChannelAndCustomer(mixed $customerId, mixed $channelId): ?CreditCardInterface { /** @phpstan-var CreditCardInterface|null */ - return $this->createByCustomerListQueryBuilder($customerId) + return $this->createQueryBuilder('o') + ->andWhere('o.customer = :customer') + ->andWhere('o.channel = :channel') + ->setParameter('customer', $customerId) + ->setParameter('channel', $channelId) ->getQuery() ->getOneOrNullResult() ; diff --git a/src/Repository/CreditCardRepositoryInterface.php b/src/Repository/CreditCardRepositoryInterface.php new file mode 100644 index 00000000..ae9f4fc9 --- /dev/null +++ b/src/Repository/CreditCardRepositoryInterface.php @@ -0,0 +1,15 @@ + Date: Wed, 30 Oct 2024 11:38:58 +0100 Subject: [PATCH 08/20] Add support for paying with saved credit cards --- assets/shop/js/card_form.js | 22 +++++ config/routes_shop.php | 6 +- config/serialization/Pay.xml | 3 + config/services/form.php | 12 +++ config/services/payum/action.php | 3 + .../AddSavedCreditCardsListener.php | 83 +++++++++++++++++++ src/Form/Type/TpayCardType.php | 4 + src/Form/Type/TpayPaymentDetailsType.php | 6 ++ src/Model/PaymentDetails.php | 13 +++ src/Payum/Action/Api/PayWithCardAction.php | 49 ++++++++--- src/Repository/CreditCardRepository.php | 30 +++++-- .../CreditCardRepositoryInterface.php | 9 +- templates/shop/payment/_card.html.twig | 13 +++ translations/messages.en.yaml | 4 + translations/messages.pl.yaml | 9 ++ 15 files changed, 243 insertions(+), 23 deletions(-) create mode 100644 src/Form/EventListener/AddSavedCreditCardsListener.php diff --git a/assets/shop/js/card_form.js b/assets/shop/js/card_form.js index 652c082f..93a8b5e0 100644 --- a/assets/shop/js/card_form.js +++ b/assets/shop/js/card_form.js @@ -4,6 +4,7 @@ const MAX_CARD_NUMBER_LENGTH = 16; export class CardForm { #form; + #savedCard; #cardNumber; #cardOperatorIcon; #cardsApi; @@ -15,6 +16,7 @@ export class CardForm { constructor(selector) { this.#form = document.querySelector(selector); + this.#savedCard = this.#form.querySelector('[data-tpay-saved-card]'); this.#cardNumber = this.#form.querySelector('[data-tpay-card-number]'); this.#cardOperatorIcon = this.#form.querySelector('[data-tpay-card-operator-icon]'); this.#cardsApi = this.#form.querySelector('[data-tpay-cards-api]'); @@ -67,18 +69,30 @@ export class CardForm { } isCvcValid() { + if (!this.shouldBeValidated()) { + return true; + } + const regex = new RegExp(/^\d{3}$/); return regex.test(this.getCardCvc()); } isCardNumberValid() { + if (!this.shouldBeValidated()) { + return true; + } + const regex = new RegExp(`^\\d{${MAX_CARD_NUMBER_LENGTH}}$`); return regex.test(this.getCardNumber()); } isExpirationMonthValid() { + if (!this.shouldBeValidated()) { + return true; + } + if (this.getExpirationYear() > new Date().getFullYear()) { return true; } @@ -87,9 +101,17 @@ export class CardForm { } isExpirationYearValid() { + if (!this.shouldBeValidated()) { + return true; + } + return this.getExpirationYear() >= new Date().getFullYear(); } + shouldBeValidated() { + return this.#savedCard.value === ''; + } + getCardHolderName() { return this.#form.querySelector('[data-tpay-card-holder-name]').value.trim(); } diff --git a/config/routes_shop.php b/config/routes_shop.php index dc7fcd61..7cef0a2f 100644 --- a/config/routes_shop.php +++ b/config/routes_shop.php @@ -46,11 +46,11 @@ '_sylius' => [ 'section' => 'shop_account', 'repository' => [ - 'method' => 'findOneByChannelAndCustomer', + 'method' => 'findOneByIdCustomerAndChannel', 'arguments' => [ '$id', - 'expr:service(\'sylius.context.customer\').getCustomer().getId()', - 'expr:service(\'sylius.context.channel\').getChannel().getId()', + 'expr:service(\'sylius.context.customer\').getCustomer()', + 'expr:service(\'sylius.context.channel\').getChannel()', ], ], 'redirect' => Routing::SHOP_ACCOUNT_CREDIT_CARD_INDEX, diff --git a/config/serialization/Pay.xml b/config/serialization/Pay.xml index bfa60d07..9c1f1e38 100644 --- a/config/serialization/Pay.xml +++ b/config/serialization/Pay.xml @@ -29,6 +29,9 @@ commerce_weavers_sylius_tpay:shop:order:pay + + commerce_weavers_sylius_tpay:shop:order:pay + commerce_weavers_sylius_tpay:shop:order:pay diff --git a/config/services/form.php b/config/services/form.php index 97ce24ad..e7f67ebd 100644 --- a/config/services/form.php +++ b/config/services/form.php @@ -4,8 +4,10 @@ namespace Symfony\Component\DependencyInjection\Loader\Configurator; +use CommerceWeavers\SyliusTpayPlugin\EventListener\AddCreditCardToAccountMenuEventListener; use CommerceWeavers\SyliusTpayPlugin\Form\DataTransformer\CardTypeDataTransformer; use CommerceWeavers\SyliusTpayPlugin\Form\DataTransformer\VisaMobilePhoneDataTransformer; +use CommerceWeavers\SyliusTpayPlugin\Form\EventListener\AddSavedCreditCardsListener; use CommerceWeavers\SyliusTpayPlugin\Form\EventListener\DecryptGatewayConfigListener; use CommerceWeavers\SyliusTpayPlugin\Form\EventListener\EncryptGatewayConfigListener; use CommerceWeavers\SyliusTpayPlugin\Form\EventListener\RemoveUnnecessaryPaymentDetailsFieldsListener; @@ -49,6 +51,7 @@ $services->set(TpayPaymentDetailsType::class) ->args([ service('commerce_weavers_sylius_tpay.form.event_listener.remove_unnecessary_payment_details_fields'), + service('commerce_weavers_sylius_tpay.form.event_listener.add_saved_credit_cards'), service('security.token_storage'), ]) ->tag('form.type') @@ -71,4 +74,13 @@ ; $services->set('commerce_weavers_sylius_tpay.form.event_listener.remove_unnecessary_payment_details_fields', RemoveUnnecessaryPaymentDetailsFieldsListener::class); + + $services + ->set('commerce_weavers_sylius_tpay.form.event_listener.add_saved_credit_cards', AddSavedCreditCardsListener::class) + ->args([ + service('security.token_storage'), + service('translator'), + service('commerce_weavers_sylius_tpay.repository.credit_card'), + ]) + ; }; diff --git a/config/services/payum/action.php b/config/services/payum/action.php index 392b085c..c46db833 100644 --- a/config/services/payum/action.php +++ b/config/services/payum/action.php @@ -103,6 +103,9 @@ ; $services->set(PayWithCardAction::class) + ->args([ + service('commerce_weavers_sylius_tpay.repository.credit_card'), + ]) ->tag('payum.action', ['factory' => TpayGatewayFactory::NAME, 'alias' => 'cw.tpay.pay_with_card']) ; diff --git a/src/Form/EventListener/AddSavedCreditCardsListener.php b/src/Form/EventListener/AddSavedCreditCardsListener.php new file mode 100644 index 00000000..ad694e16 --- /dev/null +++ b/src/Form/EventListener/AddSavedCreditCardsListener.php @@ -0,0 +1,83 @@ +getForm(); + /** @var OrderInterface|PaymentInterface|mixed $data */ + $data = $form->getParent()->getData(); + + if ($data instanceof PaymentInterface) { + $data = $data->getOrder(); + } + + if (!$data instanceof OrderInterface) { + return; + } + + $channel = $data->getChannel(); + + $token = $this->tokenStorage->getToken(); + $user = $token?->getUser(); + + $customer = $user?->getCustomer(); + + if (!$this->creditCardRepository->hasCustomerAnyCreditCardInGivenChannel($customer, $channel)) { + return; + } + + $creditCards = $this->creditCardRepository->findByCustomerAndChannel($customer, $channel); + + $choices = []; + + foreach ($creditCards as $creditCard) { + $stringifiedCard = $this->translator->trans( + 'commerce_weavers_sylius_tpay.shop.credit_card.card_selection_one_liner', + [ + '%brand%' => $creditCard->getBrand(), + '%tail%' => $creditCard->getTail(), + '%expires%' => $creditCard->getExpirationDate()->format('m-Y'), + ], 'messages' + ); + + $choices[$stringifiedCard] = $creditCard->getId(); + } + + VarDumper::dump($choices); + + $form + ->add('useSavedCreditCard', ChoiceType::class, + [ + 'label' => 'commerce_weavers_sylius_tpay.shop.order_summary.card.use_saved_credit_card.label', + 'placeholder' => new TranslatableMessage('commerce_weavers_sylius_tpay.shop.credit_card.use_new_card'), + 'required' => false, + 'choices' => $choices, + ] + ) + ; + } +} diff --git a/src/Form/Type/TpayCardType.php b/src/Form/Type/TpayCardType.php index 52495590..bf33ad31 100644 --- a/src/Form/Type/TpayCardType.php +++ b/src/Form/Type/TpayCardType.php @@ -32,6 +32,7 @@ public function buildForm(FormBuilderInterface $builder, array $options): void [ 'mapped' => false, 'label' => 'commerce_weavers_sylius_tpay.shop.order_summary.card.number', + 'required' => false, ], ) ->add( @@ -40,6 +41,7 @@ public function buildForm(FormBuilderInterface $builder, array $options): void [ 'mapped' => false, 'label' => 'commerce_weavers_sylius_tpay.shop.order_summary.card.cvv', + 'required' => false, ], ) ->add( @@ -63,6 +65,7 @@ public function buildForm(FormBuilderInterface $builder, array $options): void 'commerce_weavers_sylius_tpay.shop.order_summary.card.expiration_date.month.november' => '11', 'commerce_weavers_sylius_tpay.shop.order_summary.card.expiration_date.month.december' => '12', ], + 'required' => false, ], ) ->add( @@ -73,6 +76,7 @@ public function buildForm(FormBuilderInterface $builder, array $options): void 'label' => 'commerce_weavers_sylius_tpay.shop.order_summary.card.expiration_date.year', 'placeholder' => 'commerce_weavers_sylius_tpay.shop.order_summary.card.expiration_date.year_placeholder', 'choices' => $this->getCardValidYearsRange(), + 'required' => false, ], ) ->add('card', HiddenType::class) diff --git a/src/Form/Type/TpayPaymentDetailsType.php b/src/Form/Type/TpayPaymentDetailsType.php index d11d6aaf..f27318d8 100644 --- a/src/Form/Type/TpayPaymentDetailsType.php +++ b/src/Form/Type/TpayPaymentDetailsType.php @@ -21,6 +21,7 @@ final class TpayPaymentDetailsType extends AbstractType { public function __construct( private readonly object $removeUnnecessaryPaymentDetailsFieldsListener, + private readonly object $addSavedCreditCardsListener, private readonly TokenStorageInterface $tokenStorage, ) { } @@ -106,6 +107,11 @@ public function buildForm(FormBuilderInterface $builder, array $options): void [$this->removeUnnecessaryPaymentDetailsFieldsListener, '__invoke'], ); + $builder->addEventListener( + FormEvents::PRE_SET_DATA, + [$this->addSavedCreditCardsListener, '__invoke'], + ); + $token = $this->tokenStorage->getToken(); $user = $token?->getUser(); diff --git a/src/Model/PaymentDetails.php b/src/Model/PaymentDetails.php index 86c23990..b299fdc6 100644 --- a/src/Model/PaymentDetails.php +++ b/src/Model/PaymentDetails.php @@ -24,6 +24,7 @@ public function __construct( #[\SensitiveParameter] private ?string $encodedCardData = null, private bool $saveCreditCardForLater = false, + private ?int $useSavedCreditCard = null, #[\SensitiveParameter] private ?string $applePaySession = null, private ?string $paymentUrl = null, @@ -136,6 +137,16 @@ public function setSaveCreditCardForLater(?bool $saveCreditCardForLater): void $this->saveCreditCardForLater = $saveCreditCardForLater; } + public function getUseSavedCreditCard(): ?int + { + return $this->useSavedCreditCard; + } + + public function setUseSavedCreditCard(?int $useSavedCreditCard): void + { + $this->useSavedCreditCard = $useSavedCreditCard; + } + public function getApplePaySession(): ?string { return $this->applePaySession; @@ -245,6 +256,7 @@ public static function fromArray(array $details): self $details['tpay']['google_pay_token'] ?? null, $details['tpay']['card'] ?? null, $details['tpay']['saveCreditCardForLater'] ?? false, + $details['tpay']['useSavedCreditCard'] ?? null, $details['tpay']['apple_pay_session'] ?? null, $details['tpay']['payment_url'] ?? null, $details['tpay']['success_url'] ?? null, @@ -269,6 +281,7 @@ public function toArray(): array 'google_pay_token' => $this->googlePayToken, 'card' => $this->encodedCardData, 'saveCreditCardForLater' => $this->saveCreditCardForLater, + 'useSavedCreditCard' => $this->useSavedCreditCard, 'apple_pay_session' => $this->applePaySession, 'payment_url' => $this->paymentUrl, 'success_url' => $this->successUrl, diff --git a/src/Payum/Action/Api/PayWithCardAction.php b/src/Payum/Action/Api/PayWithCardAction.php index 0608db0f..0a594dd3 100644 --- a/src/Payum/Action/Api/PayWithCardAction.php +++ b/src/Payum/Action/Api/PayWithCardAction.php @@ -6,6 +6,7 @@ use CommerceWeavers\SyliusTpayPlugin\Model\PaymentDetails; use CommerceWeavers\SyliusTpayPlugin\Payum\Request\Api\PayWithCard; +use CommerceWeavers\SyliusTpayPlugin\Repository\CreditCardRepositoryInterface; use CommerceWeavers\SyliusTpayPlugin\Tpay\PayGroup; use Payum\Core\Reply\HttpRedirect; use Payum\Core\Request\Generic; @@ -14,28 +15,24 @@ class PayWithCardAction extends BasePaymentAwareAction { + public function __construct(private readonly CreditCardRepositoryInterface $creditCardRepository) + { + parent::__construct(); + } + protected function doExecute(Generic $request, PaymentInterface $model, PaymentDetails $paymentDetails, string $gatewayName, string $localeCode): void { Assert::notNull($paymentDetails->getEncodedCardData(), 'Card data is required to pay with card.'); Assert::notNull($paymentDetails->getTransactionId(), 'Transaction ID is required to pay with card.'); - $payload = [ - 'groupId' => PayGroup::CARD, - 'cardPaymentData' => [ - 'card' => $paymentDetails->getEncodedCardData(), - ], - ]; - - if ($paymentDetails->isSaveCreditCardForLater()) { - $payload['cardPaymentData']['save'] = true; - } + $payload = $this->getPayload($paymentDetails); $this->do( fn () => $this->api->transactions()->createPaymentByTransactionId($payload, $paymentDetails->getTransactionId()), onSuccess: function ($response) use ($paymentDetails) { $paymentDetails->setResult($response['result']); $paymentDetails->setStatus($response['status']); - $paymentDetails->setPaymentUrl($response['transactionPaymentUrl']); + $paymentDetails->setPaymentUrl($response['transactionPaymentUrl'] ?? null); }, onFailure: fn () => $paymentDetails->setStatus(PaymentInterface::STATE_FAILED), ); @@ -54,4 +51,34 @@ public function supports($request): bool { return $request instanceof PayWithCard && $request->getModel() instanceof PaymentInterface; } + + /** + * @param PaymentDetails $paymentDetails + * + * @return array + */ + private function getPayload(PaymentDetails $paymentDetails): array + { + $payload = [ + 'groupId' => PayGroup::CARD, + ]; + + if ($paymentDetails->getUseSavedCreditCard() !== null) { + $payload['cardPaymentData'] = [ + 'token' => $this->creditCardRepository->find($paymentDetails->getUseSavedCreditCard())->getToken(), + ]; + + return $payload; + } + + $payload['cardPaymentData'] = [ + 'card' => $paymentDetails->getEncodedCardData(), + ]; + + if ($paymentDetails->isSaveCreditCardForLater()) { + $payload['cardPaymentData']['save'] = true; + } + + return $payload; + } } diff --git a/src/Repository/CreditCardRepository.php b/src/Repository/CreditCardRepository.php index 0d20a715..d0f97bc2 100644 --- a/src/Repository/CreditCardRepository.php +++ b/src/Repository/CreditCardRepository.php @@ -5,6 +5,7 @@ namespace CommerceWeavers\SyliusTpayPlugin\Repository; use CommerceWeavers\SyliusTpayPlugin\Entity\CreditCardInterface; +use Doctrine\Common\Collections\Collection; use Doctrine\ORM\QueryBuilder; use Sylius\Bundle\ResourceBundle\Doctrine\ORM\EntityRepository; use Sylius\Component\Core\Model\ChannelInterface; @@ -12,7 +13,7 @@ final class CreditCardRepository extends EntityRepository implements CreditCardRepositoryInterface { - public function createByCustomerListQueryBuilder(CustomerInterface $customer, ChannelInterface $channel): QueryBuilder + public function createByCustomerListQueryBuilder(?CustomerInterface $customer, ?ChannelInterface $channel): QueryBuilder { return $this->createQueryBuilder('o') ->andWhere('o.customer = :customer') @@ -22,16 +23,31 @@ public function createByCustomerListQueryBuilder(CustomerInterface $customer, Ch ; } - public function findOneByChannelAndCustomer(mixed $customerId, mixed $channelId): ?CreditCardInterface + public function findOneByIdCustomerAndChannel(mixed $id, ?CustomerInterface $customer, ?ChannelInterface $channel): ?CreditCardInterface { /** @phpstan-var CreditCardInterface|null */ - return $this->createQueryBuilder('o') - ->andWhere('o.customer = :customer') - ->andWhere('o.channel = :channel') - ->setParameter('customer', $customerId) - ->setParameter('channel', $channelId) + return $this->createByCustomerListQueryBuilder($customer, $channel) + ->andWhere('o.id = :id') + ->setParameter('id', $id) ->getQuery() ->getOneOrNullResult() ; } + + public function findByCustomerAndChannel(?CustomerInterface $customer, ?ChannelInterface $channel): array + { + return $this->createByCustomerListQueryBuilder($customer, $channel) + ->getQuery() + ->getResult() + ; + } + + public function hasCustomerAnyCreditCardInGivenChannel(?CustomerInterface $customer, ?ChannelInterface $channel): bool + { + return 0 !== $this->createByCustomerListQueryBuilder($customer, $channel) + ->select('COUNT(o.id)') + ->getQuery() + ->getSingleScalarResult() + ; + } } diff --git a/src/Repository/CreditCardRepositoryInterface.php b/src/Repository/CreditCardRepositoryInterface.php index ae9f4fc9..d29e41b8 100644 --- a/src/Repository/CreditCardRepositoryInterface.php +++ b/src/Repository/CreditCardRepositoryInterface.php @@ -3,13 +3,18 @@ namespace CommerceWeavers\SyliusTpayPlugin\Repository; use CommerceWeavers\SyliusTpayPlugin\Entity\CreditCardInterface; +use Doctrine\Common\Collections\Collection; use Doctrine\ORM\QueryBuilder; use Sylius\Component\Core\Model\ChannelInterface; use Sylius\Component\Core\Model\CustomerInterface; interface CreditCardRepositoryInterface { - public function createByCustomerListQueryBuilder(CustomerInterface $customer, ChannelInterface $channel): QueryBuilder; + public function createByCustomerListQueryBuilder(?CustomerInterface $customer, ?ChannelInterface $channel): QueryBuilder; - public function findOneByChannelAndCustomer(mixed $customerId, mixed $channelId): ?CreditCardInterface; + public function findOneByIdCustomerAndChannel(mixed $id, ?CustomerInterface $customer, ?ChannelInterface $channel): ?CreditCardInterface; + + public function findByCustomerAndChannel(?CustomerInterface $customer, ?ChannelInterface $channel): array; + + public function hasCustomerAnyCreditCardInGivenChannel(?CustomerInterface $customer, ?ChannelInterface $channel): bool; } diff --git a/templates/shop/payment/_card.html.twig b/templates/shop/payment/_card.html.twig index 6d84dce8..40172dae 100644 --- a/templates/shop/payment/_card.html.twig +++ b/templates/shop/payment/_card.html.twig @@ -2,6 +2,19 @@
+ {% if form.tpay.useSavedCreditCard is defined %} +
+
+ {{ form_label(form.tpay.useSavedCreditCard) }} +
+ {{ form_widget(form.tpay.useSavedCreditCard, { attr: {'data-tpay-saved-card': ''}}) }} +
+
+
+
+ {{ 'sylius.ui.or'|trans }} +
+ {% endif %}
{{ form_label(form.tpay.card.number) }} diff --git a/translations/messages.en.yaml b/translations/messages.en.yaml index ce495616..056edc79 100644 --- a/translations/messages.en.yaml +++ b/translations/messages.en.yaml @@ -24,6 +24,8 @@ commerce_weavers_sylius_tpay: brand: 'Brand' expiration_date: 'Expiration date' tail: 'Tail' + card_selection_one_liner: 'Brand: %brand%, last 4 numbers: %tail%, expires: %expires%' + use_new_card: 'Use new card' order_summary: blik: token: 'BLIK token' @@ -51,6 +53,8 @@ commerce_weavers_sylius_tpay: number: 'Number' save_credit_card_for_later: label: 'Save card for later' + use_saved_credit_card: + label: 'Saved card' visa_mobile: placeholder: 'Phone number' payment_failed: diff --git a/translations/messages.pl.yaml b/translations/messages.pl.yaml index 489ca428..519415e5 100644 --- a/translations/messages.pl.yaml +++ b/translations/messages.pl.yaml @@ -19,6 +19,13 @@ commerce_weavers_sylius_tpay: redirect: 'Przekierowanie do Tpay' pay_by_link: 'Przekierowanie do banku' shop: + credit_cards: 'Karty kredytowe' + credit_card: + brand: 'Marka' + expiration_date: 'Data ważności' + tail: 'Ostatnie cyfry' + card_selection_one_liner: 'Marka: %brand%, ostatnie 4 cyfry: %tail%, ważność: %expires%' + use_new_card: 'Użyj nowej karty' order_summary: blik: token: 'Kod BLIK' @@ -46,6 +53,8 @@ commerce_weavers_sylius_tpay: number: 'Numer' save_credit_card_for_later: label: 'Zapisz kartę na później' + use_saved_credit_card: + label: 'Zapisane karty' visa_mobile: placeholder: 'Numer telefonu' payment_failed: From def884baea8bf99f546b9a078000ee735e44029a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=81ukasz=20Chru=C5=9Bciel?= Date: Wed, 30 Oct 2024 12:06:09 +0100 Subject: [PATCH 09/20] Extract mapper to separate class --- config/services/payum/action.php | 2 +- config/services/payum/mapper.php | 38 ++++++++ migrations/Version20241030113142.php | 31 ++++++ src/Entity/CreditCard.php | 12 +++ src/Entity/CreditCardInterface.php | 4 + src/Payum/Action/Api/PayWithCardAction.php | 37 +------- src/Payum/Action/Api/SaveCreditCardAction.php | 2 + .../Mapper/PayWithCardActionPayloadMapper.php | 52 ++++++++++ ...ayWithCardActionPayloadMapperInterface.php | 15 +++ .../CreditCardRepositoryInterface.php | 4 +- .../PayWithCardActionPayloadMapperTest.php | 94 +++++++++++++++++++ 11 files changed, 254 insertions(+), 37 deletions(-) create mode 100644 config/services/payum/mapper.php create mode 100644 migrations/Version20241030113142.php create mode 100644 src/Payum/Mapper/PayWithCardActionPayloadMapper.php create mode 100644 src/Payum/Mapper/PayWithCardActionPayloadMapperInterface.php create mode 100644 tests/Unit/Payum/Mapper/PayWithCardActionPayloadMapperTest.php diff --git a/config/services/payum/action.php b/config/services/payum/action.php index c46db833..fdf2a72e 100644 --- a/config/services/payum/action.php +++ b/config/services/payum/action.php @@ -104,7 +104,7 @@ $services->set(PayWithCardAction::class) ->args([ - service('commerce_weavers_sylius_tpay.repository.credit_card'), + service('commerce_weavers_sylius_tpay.payum.mapper.pay_with_card_action'), ]) ->tag('payum.action', ['factory' => TpayGatewayFactory::NAME, 'alias' => 'cw.tpay.pay_with_card']) ; diff --git a/config/services/payum/mapper.php b/config/services/payum/mapper.php new file mode 100644 index 00000000..8bd3ec26 --- /dev/null +++ b/config/services/payum/mapper.php @@ -0,0 +1,38 @@ +services(); + $services->defaults() + ->public() + ; + + $services->set('commerce_weavers_sylius_tpay.payum.mapper.pay_with_card_action', PayWithCardActionPayloadMapper::class) + ->args([ + service('commerce_weavers_sylius_tpay.repository.credit_card'), + ]) + ; +}; diff --git a/migrations/Version20241030113142.php b/migrations/Version20241030113142.php new file mode 100644 index 00000000..881bab72 --- /dev/null +++ b/migrations/Version20241030113142.php @@ -0,0 +1,31 @@ +addSql('ALTER TABLE cw_sylius_tpay_blik_alias CHANGE registered registered TINYINT(1) DEFAULT false NOT NULL'); + } + + public function down(Schema $schema): void + { + // this down() migration is auto-generated, please modify it to your needs + $this->addSql('ALTER TABLE cw_sylius_tpay_blik_alias CHANGE registered registered TINYINT(1) DEFAULT 0 NOT NULL'); + } +} diff --git a/src/Entity/CreditCard.php b/src/Entity/CreditCard.php index 59cdf7d0..a4f37ae4 100644 --- a/src/Entity/CreditCard.php +++ b/src/Entity/CreditCard.php @@ -11,6 +11,8 @@ class CreditCard implements CreditCardInterface { private ?int $id = null; + private ?string $uid = null; + private ?string $token = null; private ?string $brand = null; private ?string $tail = null; @@ -26,6 +28,16 @@ public function getId(): ?int return $this->id; } + public function getUid(): ?string + { + return $this->uid; + } + + public function setUid(?string $uid): void + { + $this->uid = $uid; + } + public function getToken(): ?string { return $this->token; diff --git a/src/Entity/CreditCardInterface.php b/src/Entity/CreditCardInterface.php index 10c33077..a3379d7c 100644 --- a/src/Entity/CreditCardInterface.php +++ b/src/Entity/CreditCardInterface.php @@ -7,6 +7,10 @@ interface CreditCardInterface extends ResourceInterface { + public function getUid(): ?string; + + public function setUid(?string $uid): void; + public function getToken(): ?string; public function setToken(?string $token): void; diff --git a/src/Payum/Action/Api/PayWithCardAction.php b/src/Payum/Action/Api/PayWithCardAction.php index 0a594dd3..4d723696 100644 --- a/src/Payum/Action/Api/PayWithCardAction.php +++ b/src/Payum/Action/Api/PayWithCardAction.php @@ -5,9 +5,8 @@ namespace CommerceWeavers\SyliusTpayPlugin\Payum\Action\Api; use CommerceWeavers\SyliusTpayPlugin\Model\PaymentDetails; +use CommerceWeavers\SyliusTpayPlugin\Payum\Mapper\PayWithCardActionPayloadMapperInterface; use CommerceWeavers\SyliusTpayPlugin\Payum\Request\Api\PayWithCard; -use CommerceWeavers\SyliusTpayPlugin\Repository\CreditCardRepositoryInterface; -use CommerceWeavers\SyliusTpayPlugin\Tpay\PayGroup; use Payum\Core\Reply\HttpRedirect; use Payum\Core\Request\Generic; use Sylius\Component\Core\Model\PaymentInterface; @@ -15,7 +14,7 @@ class PayWithCardAction extends BasePaymentAwareAction { - public function __construct(private readonly CreditCardRepositoryInterface $creditCardRepository) + public function __construct(private readonly PayWithCardActionPayloadMapperInterface $payWithCardActionPayloadMapper) { parent::__construct(); } @@ -25,7 +24,7 @@ protected function doExecute(Generic $request, PaymentInterface $model, PaymentD Assert::notNull($paymentDetails->getEncodedCardData(), 'Card data is required to pay with card.'); Assert::notNull($paymentDetails->getTransactionId(), 'Transaction ID is required to pay with card.'); - $payload = $this->getPayload($paymentDetails); + $payload = $this->payWithCardActionPayloadMapper->getPayload($paymentDetails); $this->do( fn () => $this->api->transactions()->createPaymentByTransactionId($payload, $paymentDetails->getTransactionId()), @@ -51,34 +50,4 @@ public function supports($request): bool { return $request instanceof PayWithCard && $request->getModel() instanceof PaymentInterface; } - - /** - * @param PaymentDetails $paymentDetails - * - * @return array - */ - private function getPayload(PaymentDetails $paymentDetails): array - { - $payload = [ - 'groupId' => PayGroup::CARD, - ]; - - if ($paymentDetails->getUseSavedCreditCard() !== null) { - $payload['cardPaymentData'] = [ - 'token' => $this->creditCardRepository->find($paymentDetails->getUseSavedCreditCard())->getToken(), - ]; - - return $payload; - } - - $payload['cardPaymentData'] = [ - 'card' => $paymentDetails->getEncodedCardData(), - ]; - - if ($paymentDetails->isSaveCreditCardForLater()) { - $payload['cardPaymentData']['save'] = true; - } - - return $payload; - } } diff --git a/src/Payum/Action/Api/SaveCreditCardAction.php b/src/Payum/Action/Api/SaveCreditCardAction.php index cbf24287..9c18848e 100644 --- a/src/Payum/Action/Api/SaveCreditCardAction.php +++ b/src/Payum/Action/Api/SaveCreditCardAction.php @@ -13,6 +13,7 @@ use Sylius\Component\Core\Model\PaymentInterface; use Sylius\Component\Resource\Repository\RepositoryInterface; use Sylius\Resource\Factory\FactoryInterface; +use Symfony\Component\Uid\Uuid; final class SaveCreditCardAction extends BasePaymentAwareAction implements GatewayAwareInterface { @@ -33,6 +34,7 @@ protected function doExecute(Generic $request, PaymentInterface $model, PaymentD /** @var CreditCardInterface $creditCard */ $creditCard = $this->creditCardFactory->createNew(); + $creditCard->setUid(Uuid::v4()->toRfc4122()); $creditCard->setToken($request->cardToken); $creditCard->setBrand($request->cardBrand); $creditCard->setTail($request->cardTail); diff --git a/src/Payum/Mapper/PayWithCardActionPayloadMapper.php b/src/Payum/Mapper/PayWithCardActionPayloadMapper.php new file mode 100644 index 00000000..8c0c304a --- /dev/null +++ b/src/Payum/Mapper/PayWithCardActionPayloadMapper.php @@ -0,0 +1,52 @@ + string, 'cardPaymentData' => array> + */ + public function getPayload(PaymentDetails $paymentDetails): array + { + $payload = [ + 'groupId' => PayGroup::CARD, + ]; + + if ($paymentDetails->getUseSavedCreditCard() !== null) { + $payload['cardPaymentData'] = [ + 'token' => $this->creditCardRepository->find($paymentDetails->getUseSavedCreditCard())->getToken(), + ]; + + return $payload; + } + + $payload['cardPaymentData'] = [ + 'card' => $paymentDetails->getEncodedCardData(), + ]; + + if ($paymentDetails->isSaveCreditCardForLater()) { + $payload['cardPaymentData']['save'] = true; + } + + return $payload; + } +} diff --git a/src/Payum/Mapper/PayWithCardActionPayloadMapperInterface.php b/src/Payum/Mapper/PayWithCardActionPayloadMapperInterface.php new file mode 100644 index 00000000..b25c2574 --- /dev/null +++ b/src/Payum/Mapper/PayWithCardActionPayloadMapperInterface.php @@ -0,0 +1,15 @@ + string, 'cardPaymentData' => array> + */ + public function getPayload(PaymentDetails $paymentDetails): array; +} diff --git a/src/Repository/CreditCardRepositoryInterface.php b/src/Repository/CreditCardRepositoryInterface.php index d29e41b8..32b01f6a 100644 --- a/src/Repository/CreditCardRepositoryInterface.php +++ b/src/Repository/CreditCardRepositoryInterface.php @@ -3,12 +3,12 @@ namespace CommerceWeavers\SyliusTpayPlugin\Repository; use CommerceWeavers\SyliusTpayPlugin\Entity\CreditCardInterface; -use Doctrine\Common\Collections\Collection; use Doctrine\ORM\QueryBuilder; use Sylius\Component\Core\Model\ChannelInterface; use Sylius\Component\Core\Model\CustomerInterface; +use Sylius\Component\Resource\Repository\RepositoryInterface; -interface CreditCardRepositoryInterface +interface CreditCardRepositoryInterface extends RepositoryInterface { public function createByCustomerListQueryBuilder(?CustomerInterface $customer, ?ChannelInterface $channel): QueryBuilder; diff --git a/tests/Unit/Payum/Mapper/PayWithCardActionPayloadMapperTest.php b/tests/Unit/Payum/Mapper/PayWithCardActionPayloadMapperTest.php new file mode 100644 index 00000000..2a407e02 --- /dev/null +++ b/tests/Unit/Payum/Mapper/PayWithCardActionPayloadMapperTest.php @@ -0,0 +1,94 @@ +creditCardRepository = $this->prophesize(CreditCardRepositoryInterface::class); + } + + public function test_it_returns_payload_for_card_payment(): void + { + $paymentDetails = $this->prophesize(PaymentDetails::class); + + $paymentDetails->getUseSavedCreditCard()->willReturn(null); + $paymentDetails->getEncodedCardData()->willReturn('encoded_card_data'); + $paymentDetails->isSaveCreditCardForLater()->willReturn(false); + + $mapper = $this->createTestSubject(); + $payload = $mapper->getPayload($paymentDetails->reveal()); + + $this->assertSame([ + 'groupId' => PayGroup::CARD, + 'cardPaymentData' => [ + 'card' => 'encoded_card_data', + ], + ], $payload); + } + + public function test_it_returns_payload_for_saved_card_payment(): void + { + $paymentDetails = $this->prophesize(PaymentDetails::class); + + $paymentDetails->getUseSavedCreditCard()->willReturn(1); + + $creditCard = $this->prophesize(CreditCardInterface::class); + $creditCard->getToken()->willReturn('token'); + + $this->creditCardRepository->find(1)->willReturn($creditCard->reveal()); + + $mapper = $this->createTestSubject(); + $payload = $mapper->getPayload($paymentDetails->reveal()); + + $this->assertSame([ + 'groupId' => PayGroup::CARD, + 'cardPaymentData' => [ + 'token' => 'token', + ], + ], $payload); + } + + public function test_it_returns_payload_for_card_payment_and_save_for_later(): void + { + $paymentDetails = $this->prophesize(PaymentDetails::class); + + $paymentDetails->getUseSavedCreditCard()->willReturn(null); + $paymentDetails->isSaveCreditCardForLater()->willReturn(true); + $paymentDetails->getEncodedCardData()->willReturn('encoded_card_data'); + + $mapper = $this->createTestSubject(); + $payload = $mapper->getPayload($paymentDetails->reveal()); + + $this->assertSame([ + 'groupId' => PayGroup::CARD, + 'cardPaymentData' => [ + 'card' => 'encoded_card_data', + 'save' => true, + ], + ], $payload); + } + + private function createTestSubject(): PayWithCardActionPayloadMapper + { + return new PayWithCardActionPayloadMapper( + $this->creditCardRepository->reveal() + ); + } +} From 5f8b667c2ddce51ea4ae5ba338daac687371ff96 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=81ukasz=20Chru=C5=9Bciel?= Date: Wed, 30 Oct 2024 17:22:00 +0100 Subject: [PATCH 10/20] Improve coding standards --- config/services/event_listener.php | 1 - phpstan.neon | 1 + .../CreditCardShopUserCollectionExtension.php | 3 +- .../CreditCardShopUserItemExtension.php | 1 - src/DependencyInjection/Configuration.php | 1 - src/Entity/CreditCard.php | 3 ++ src/Entity/CreditCardInterface.php | 2 ++ .../AddSavedCreditCardsListener.php | 21 ++++++++++---- src/Form/Type/TpayPaymentDetailsType.php | 6 ++-- src/Model/PaymentDetails.php | 2 +- src/Payum/Action/Api/SaveCreditCardAction.php | 27 ++++++++++++----- .../Mapper/PayWithCardActionPayloadMapper.php | 17 +++++------ ...ayWithCardActionPayloadMapperInterface.php | 6 ++-- src/Payum/Request/Api/SaveCreditCard.php | 29 +++++++++++++++---- src/Repository/CreditCardRepository.php | 8 +++-- .../CreditCardRepositoryInterface.php | 2 ++ 16 files changed, 91 insertions(+), 39 deletions(-) diff --git a/config/services/event_listener.php b/config/services/event_listener.php index 316f3607..f3cab137 100644 --- a/config/services/event_listener.php +++ b/config/services/event_listener.php @@ -14,6 +14,5 @@ $services->set(AddCreditCardToAccountMenuEventListener::class) ->tag('kernel.event_listener', ['event' => AccountMenuBuilder::EVENT_NAME, 'method' => '__invoke']) - ; }; diff --git a/phpstan.neon b/phpstan.neon index 6c51955a..b0ac1fd7 100644 --- a/phpstan.neon +++ b/phpstan.neon @@ -17,6 +17,7 @@ parameters: identifier: missingType.generics # ignore missing type generics, as it is complicated when supported for Sylius 1.12 and 1.13 is required - '/Parameter #1 \$configuration of method Symfony\\Component\\DependencyInjection\\Extension\\Extension::processConfiguration\(\) expects Symfony\\Component\\Config\\Definition\\ConfigurationInterface, Symfony\\Component\\Config\\Definition\\ConfigurationInterface\|null given\./' - '/Parameter \#1 \$request \([^)]+\) of method [^:]+::execute\(\) should be contravariant with parameter \$request \(mixed\) of method Payum\\Core\\Action\\ActionInterface::execute\(\)/' + - '/Parameter \#1 \$request \([^)]+\) of method [^:]+::doExecute\(\) should be contravariant with parameter \$request \([^)]+\) of method CommerceWeavers\\SyliusTpayPlugin\\Payum\\Action\\Api\\BasePaymentAwareAction::doExecute\(\)/' - '/Parameter \$event of method CommerceWeavers\\SyliusTpayPlugin\\Refunding\\Workflow\\Listener\\DispatchRefundListener::__invoke\(\) has invalid type Symfony\\Component\\Workflow\\Event\\TransitionEvent\./' - '/Call to method getSubject\(\) on an unknown class Symfony\\Component\\Workflow\\Event\\TransitionEvent\./' - '#Class CommerceWeavers\\SyliusTpayPlugin\\Form\\DataTransformer\\CardTypeDataTransformer implements generic interface Symfony\\Component\\Form\\DataTransformerInterface but does not specify its types: TValue, TTransformedValue#' diff --git a/src/Api/Doctrine/QueryCollectionExtension/CreditCardShopUserCollectionExtension.php b/src/Api/Doctrine/QueryCollectionExtension/CreditCardShopUserCollectionExtension.php index d934b5c1..34e2446f 100644 --- a/src/Api/Doctrine/QueryCollectionExtension/CreditCardShopUserCollectionExtension.php +++ b/src/Api/Doctrine/QueryCollectionExtension/CreditCardShopUserCollectionExtension.php @@ -11,6 +11,7 @@ use Sylius\Bundle\ApiBundle\Context\UserContextInterface; use Sylius\Component\Channel\Context\ChannelContextInterface; use Sylius\Component\Core\Model\AdminUserInterface; +use Sylius\Component\Core\Model\ShopUserInterface; final class CreditCardShopUserCollectionExtension implements ContextAwareQueryCollectionExtensionInterface { @@ -20,7 +21,6 @@ public function __construct( ) { } - /** @param array $context */ public function applyToCollection( QueryBuilder $queryBuilder, LegacyQueryNameGeneratorInterface $queryNameGenerator, @@ -32,6 +32,7 @@ public function applyToCollection( return; } + /** @var ShopUserInterface|AdminUserInterface|null $user */ $user = $this->userContext->getUser(); if ($user instanceof AdminUserInterface) { diff --git a/src/Api/Doctrine/QueryItemExtension/CreditCardShopUserItemExtension.php b/src/Api/Doctrine/QueryItemExtension/CreditCardShopUserItemExtension.php index 970a31a9..44b4e5eb 100644 --- a/src/Api/Doctrine/QueryItemExtension/CreditCardShopUserItemExtension.php +++ b/src/Api/Doctrine/QueryItemExtension/CreditCardShopUserItemExtension.php @@ -12,7 +12,6 @@ use Sylius\Component\Channel\Context\ChannelContextInterface; use Sylius\Component\Core\Model\AdminUserInterface; use Sylius\Component\Core\Model\ShopUserInterface; -use Symfony\Component\VarDumper\VarDumper; final class CreditCardShopUserItemExtension implements QueryItemExtensionInterface { diff --git a/src/DependencyInjection/Configuration.php b/src/DependencyInjection/Configuration.php index ad9ce81e..d44f5b6b 100644 --- a/src/DependencyInjection/Configuration.php +++ b/src/DependencyInjection/Configuration.php @@ -12,7 +12,6 @@ use CommerceWeavers\SyliusTpayPlugin\Repository\BlikAliasRepository; use CommerceWeavers\SyliusTpayPlugin\Repository\CreditCardRepository; use Sylius\Bundle\ResourceBundle\Controller\ResourceController; -use Sylius\Bundle\ResourceBundle\Doctrine\ORM\EntityRepository; use Sylius\Resource\Factory\Factory; use Symfony\Component\Config\Definition\Builder\ArrayNodeDefinition; use Symfony\Component\Config\Definition\Builder\TreeBuilder; diff --git a/src/Entity/CreditCard.php b/src/Entity/CreditCard.php index a4f37ae4..b304b6f2 100644 --- a/src/Entity/CreditCard.php +++ b/src/Entity/CreditCard.php @@ -14,7 +14,9 @@ class CreditCard implements CreditCardInterface private ?string $uid = null; private ?string $token = null; + private ?string $brand = null; + private ?string $tail = null; private ?\DateTimeInterface $expirationDate = null; @@ -67,6 +69,7 @@ public function setTail(?string $tail): void { $this->tail = $tail; } + public function getExpirationDate(): ?\DateTimeInterface { return $this->expirationDate; diff --git a/src/Entity/CreditCardInterface.php b/src/Entity/CreditCardInterface.php index a3379d7c..6c46c691 100644 --- a/src/Entity/CreditCardInterface.php +++ b/src/Entity/CreditCardInterface.php @@ -1,5 +1,7 @@ getForm(); + /** @var FormInterface $form */ + $form = $form->getParent(); + /** @var OrderInterface|PaymentInterface|mixed $data */ - $data = $form->getParent()->getData(); + $data = $form->getData(); if ($data instanceof PaymentInterface) { $data = $data->getOrder(); @@ -42,8 +46,10 @@ public function __invoke(FormEvent $event): void $channel = $data->getChannel(); $token = $this->tokenStorage->getToken(); + /** @var ShopUserInterface|null $user */ $user = $token?->getUser(); + /** @var CustomerInterface $customer */ $customer = $user?->getCustomer(); if (!$this->creditCardRepository->hasCustomerAnyCreditCardInGivenChannel($customer, $channel)) { @@ -61,7 +67,8 @@ public function __invoke(FormEvent $event): void '%brand%' => $creditCard->getBrand(), '%tail%' => $creditCard->getTail(), '%expires%' => $creditCard->getExpirationDate()->format('m-Y'), - ], 'messages' + ], + 'messages', ); $choices[$stringifiedCard] = $creditCard->getId(); @@ -70,13 +77,15 @@ public function __invoke(FormEvent $event): void VarDumper::dump($choices); $form - ->add('useSavedCreditCard', ChoiceType::class, + ->add( + 'useSavedCreditCard', + ChoiceType::class, [ 'label' => 'commerce_weavers_sylius_tpay.shop.order_summary.card.use_saved_credit_card.label', 'placeholder' => new TranslatableMessage('commerce_weavers_sylius_tpay.shop.credit_card.use_new_card'), 'required' => false, 'choices' => $choices, - ] + ], ) ; } diff --git a/src/Form/Type/TpayPaymentDetailsType.php b/src/Form/Type/TpayPaymentDetailsType.php index f27318d8..af50841b 100644 --- a/src/Form/Type/TpayPaymentDetailsType.php +++ b/src/Form/Type/TpayPaymentDetailsType.php @@ -117,10 +117,12 @@ public function buildForm(FormBuilderInterface $builder, array $options): void if ($user instanceof ShopUserInterface) { $builder - ->add('saveCreditCardForLater', CheckboxType::class, + ->add( + 'saveCreditCardForLater', + CheckboxType::class, [ 'label' => 'commerce_weavers_sylius_tpay.shop.order_summary.card.save_credit_card_for_later.label', - ] + ], ) ; } diff --git a/src/Model/PaymentDetails.php b/src/Model/PaymentDetails.php index b299fdc6..02d63df6 100644 --- a/src/Model/PaymentDetails.php +++ b/src/Model/PaymentDetails.php @@ -132,7 +132,7 @@ public function isSaveCreditCardForLater(): bool return $this->saveCreditCardForLater; } - public function setSaveCreditCardForLater(?bool $saveCreditCardForLater): void + public function setSaveCreditCardForLater(bool $saveCreditCardForLater): void { $this->saveCreditCardForLater = $saveCreditCardForLater; } diff --git a/src/Payum/Action/Api/SaveCreditCardAction.php b/src/Payum/Action/Api/SaveCreditCardAction.php index 9c18848e..bfc028a1 100644 --- a/src/Payum/Action/Api/SaveCreditCardAction.php +++ b/src/Payum/Action/Api/SaveCreditCardAction.php @@ -10,10 +10,13 @@ use Payum\Core\GatewayAwareInterface; use Payum\Core\GatewayAwareTrait; use Payum\Core\Request\Generic; +use Sylius\Component\Core\Model\CustomerInterface; +use Sylius\Component\Core\Model\OrderInterface; use Sylius\Component\Core\Model\PaymentInterface; use Sylius\Component\Resource\Repository\RepositoryInterface; use Sylius\Resource\Factory\FactoryInterface; use Symfony\Component\Uid\Uuid; +use Webmozart\Assert\Assert; final class SaveCreditCardAction extends BasePaymentAwareAction implements GatewayAwareInterface { @@ -35,16 +38,26 @@ protected function doExecute(Generic $request, PaymentInterface $model, PaymentD $creditCard = $this->creditCardFactory->createNew(); $creditCard->setUid(Uuid::v4()->toRfc4122()); - $creditCard->setToken($request->cardToken); - $creditCard->setBrand($request->cardBrand); - $creditCard->setTail($request->cardTail); - $creditCard->setCustomer($model->getOrder()->getCustomer()); + $creditCard->setToken($request->getCardToken()); + $creditCard->setBrand($request->getCardBrand()); + $creditCard->setTail($request->getCardTail()); + + /** @var ?OrderInterface $order */ + $order = $model->getOrder(); + $customer = $order?->getCustomer(); + + Assert::isInstanceOf($customer, CustomerInterface::class); + + $creditCard->setCustomer($customer); + + $expiryDate = $request->getTokenExpiryDate(); + $creditCard->setExpirationDate(new \DateTimeImmutable( sprintf( '01-%s-20%s', - substr($request->tokenExpiryDate, 0, 2), - substr($request->tokenExpiryDate, 2, 2) - ) + substr($expiryDate, 0, 2), + substr($expiryDate, 2, 2), + ), )); $this->creditCardRepository->add($creditCard); diff --git a/src/Payum/Mapper/PayWithCardActionPayloadMapper.php b/src/Payum/Mapper/PayWithCardActionPayloadMapper.php index 8c0c304a..4fe7fd19 100644 --- a/src/Payum/Mapper/PayWithCardActionPayloadMapper.php +++ b/src/Payum/Mapper/PayWithCardActionPayloadMapper.php @@ -4,14 +4,10 @@ namespace CommerceWeavers\SyliusTpayPlugin\Payum\Mapper; +use CommerceWeavers\SyliusTpayPlugin\Entity\CreditCardInterface; use CommerceWeavers\SyliusTpayPlugin\Model\PaymentDetails; -use CommerceWeavers\SyliusTpayPlugin\Payum\Action\Api\BasePaymentAwareAction; -use CommerceWeavers\SyliusTpayPlugin\Payum\Request\Api\PayWithCard; use CommerceWeavers\SyliusTpayPlugin\Repository\CreditCardRepositoryInterface; use CommerceWeavers\SyliusTpayPlugin\Tpay\PayGroup; -use Payum\Core\Reply\HttpRedirect; -use Payum\Core\Request\Generic; -use Sylius\Component\Core\Model\PaymentInterface; use Webmozart\Assert\Assert; class PayWithCardActionPayloadMapper implements PayWithCardActionPayloadMapperInterface @@ -21,9 +17,7 @@ public function __construct(private readonly CreditCardRepositoryInterface $cred } /** - * @param PaymentDetails $paymentDetails - * - * @return array<'groupId' => string, 'cardPaymentData' => array> + * @return array{'groupId': int, 'cardPaymentData': array} */ public function getPayload(PaymentDetails $paymentDetails): array { @@ -32,8 +26,13 @@ public function getPayload(PaymentDetails $paymentDetails): array ]; if ($paymentDetails->getUseSavedCreditCard() !== null) { + /** @var CreditCardInterface|null $creditCard */ + $creditCard = $this->creditCardRepository->find($paymentDetails->getUseSavedCreditCard()); + + Assert::notNull($creditCard); + $payload['cardPaymentData'] = [ - 'token' => $this->creditCardRepository->find($paymentDetails->getUseSavedCreditCard())->getToken(), + 'token' => $creditCard->getToken(), ]; return $payload; diff --git a/src/Payum/Mapper/PayWithCardActionPayloadMapperInterface.php b/src/Payum/Mapper/PayWithCardActionPayloadMapperInterface.php index b25c2574..3a43ec43 100644 --- a/src/Payum/Mapper/PayWithCardActionPayloadMapperInterface.php +++ b/src/Payum/Mapper/PayWithCardActionPayloadMapperInterface.php @@ -1,5 +1,7 @@ string, 'cardPaymentData' => array> + * @return array{'groupId': int, 'cardPaymentData': array} */ public function getPayload(PaymentDetails $paymentDetails): array; } diff --git a/src/Payum/Request/Api/SaveCreditCard.php b/src/Payum/Request/Api/SaveCreditCard.php index 1887f097..8fdf48d4 100644 --- a/src/Payum/Request/Api/SaveCreditCard.php +++ b/src/Payum/Request/Api/SaveCreditCard.php @@ -10,12 +10,31 @@ class SaveCreditCard extends Generic { public function __construct( mixed $model, - public readonly string $cardToken, - public readonly string $cardBrand, - public readonly string $cardTail, - public readonly string $tokenExpiryDate, - + private readonly string $cardToken, + private readonly string $cardBrand, + private readonly string $cardTail, + private readonly string $tokenExpiryDate, ) { parent::__construct($model); } + + public function getCardToken(): string + { + return $this->cardToken; + } + + public function getCardBrand(): string + { + return $this->cardBrand; + } + + public function getCardTail(): string + { + return $this->cardTail; + } + + public function getTokenExpiryDate(): string + { + return $this->tokenExpiryDate; + } } diff --git a/src/Repository/CreditCardRepository.php b/src/Repository/CreditCardRepository.php index d0f97bc2..5960935a 100644 --- a/src/Repository/CreditCardRepository.php +++ b/src/Repository/CreditCardRepository.php @@ -5,11 +5,11 @@ namespace CommerceWeavers\SyliusTpayPlugin\Repository; use CommerceWeavers\SyliusTpayPlugin\Entity\CreditCardInterface; -use Doctrine\Common\Collections\Collection; use Doctrine\ORM\QueryBuilder; use Sylius\Bundle\ResourceBundle\Doctrine\ORM\EntityRepository; use Sylius\Component\Core\Model\ChannelInterface; use Sylius\Component\Core\Model\CustomerInterface; +use Webmozart\Assert\Assert; final class CreditCardRepository extends EntityRepository implements CreditCardRepositoryInterface { @@ -36,10 +36,14 @@ public function findOneByIdCustomerAndChannel(mixed $id, ?CustomerInterface $cus public function findByCustomerAndChannel(?CustomerInterface $customer, ?ChannelInterface $channel): array { - return $this->createByCustomerListQueryBuilder($customer, $channel) + $result = $this->createByCustomerListQueryBuilder($customer, $channel) ->getQuery() ->getResult() ; + + Assert::isArray($result); + + return $result; } public function hasCustomerAnyCreditCardInGivenChannel(?CustomerInterface $customer, ?ChannelInterface $channel): bool diff --git a/src/Repository/CreditCardRepositoryInterface.php b/src/Repository/CreditCardRepositoryInterface.php index 32b01f6a..25281bb5 100644 --- a/src/Repository/CreditCardRepositoryInterface.php +++ b/src/Repository/CreditCardRepositoryInterface.php @@ -1,5 +1,7 @@ Date: Wed, 30 Oct 2024 18:10:46 +0100 Subject: [PATCH 11/20] Add support for new flag in tests --- src/Model/PaymentDetails.php | 8 +- src/Payum/Action/Api/PayWithCardAction.php | 7 +- tests/Helper/PaymentDetailsHelperTrait.php | 3 +- .../Unit/Api/Command/PayByCardHandlerTest.php | 2 +- .../Action/Api/PayWithCardActionTest.php | 91 +++---------------- .../Action/Api/SaveCreditCardActionTest.php | 2 + 6 files changed, 28 insertions(+), 85 deletions(-) diff --git a/src/Model/PaymentDetails.php b/src/Model/PaymentDetails.php index 02d63df6..b5167a62 100644 --- a/src/Model/PaymentDetails.php +++ b/src/Model/PaymentDetails.php @@ -255,8 +255,8 @@ public static function fromArray(array $details): self $details['tpay']['blik_alias_application_code'] ?? null, $details['tpay']['google_pay_token'] ?? null, $details['tpay']['card'] ?? null, - $details['tpay']['saveCreditCardForLater'] ?? false, - $details['tpay']['useSavedCreditCard'] ?? null, + $details['tpay']['save_credit_card_for_later'] ?? false, + $details['tpay']['use_saved_credit_card'] ?? null, $details['tpay']['apple_pay_session'] ?? null, $details['tpay']['payment_url'] ?? null, $details['tpay']['success_url'] ?? null, @@ -280,8 +280,8 @@ public function toArray(): array 'blik_alias_application_code' => $this->blikAliasApplicationCode, 'google_pay_token' => $this->googlePayToken, 'card' => $this->encodedCardData, - 'saveCreditCardForLater' => $this->saveCreditCardForLater, - 'useSavedCreditCard' => $this->useSavedCreditCard, + 'save_credit_card_for_later' => $this->saveCreditCardForLater, + 'use_saved_credit_card' => $this->useSavedCreditCard, 'apple_pay_session' => $this->applePaySession, 'payment_url' => $this->paymentUrl, 'success_url' => $this->successUrl, diff --git a/src/Payum/Action/Api/PayWithCardAction.php b/src/Payum/Action/Api/PayWithCardAction.php index 4d723696..1e858d0c 100644 --- a/src/Payum/Action/Api/PayWithCardAction.php +++ b/src/Payum/Action/Api/PayWithCardAction.php @@ -24,10 +24,11 @@ protected function doExecute(Generic $request, PaymentInterface $model, PaymentD Assert::notNull($paymentDetails->getEncodedCardData(), 'Card data is required to pay with card.'); Assert::notNull($paymentDetails->getTransactionId(), 'Transaction ID is required to pay with card.'); - $payload = $this->payWithCardActionPayloadMapper->getPayload($paymentDetails); - $this->do( - fn () => $this->api->transactions()->createPaymentByTransactionId($payload, $paymentDetails->getTransactionId()), + fn () => $this->api->transactions()->createPaymentByTransactionId( + $this->payWithCardActionPayloadMapper->getPayload($paymentDetails), + $paymentDetails->getTransactionId(), + ), onSuccess: function ($response) use ($paymentDetails) { $paymentDetails->setResult($response['result']); $paymentDetails->setStatus($response['status']); diff --git a/tests/Helper/PaymentDetailsHelperTrait.php b/tests/Helper/PaymentDetailsHelperTrait.php index 17de4798..98943f54 100644 --- a/tests/Helper/PaymentDetailsHelperTrait.php +++ b/tests/Helper/PaymentDetailsHelperTrait.php @@ -19,7 +19,8 @@ protected function getExpectedDetails(...$overriddenDetails): array 'blik_alias_application_code' => null, 'google_pay_token' => null, 'card' => null, - 'saveCreditCardForLater' => false, + 'save_credit_card_for_later' => false, + 'use_saved_credit_card' => null, 'apple_pay_session' => null, 'payment_url' => null, 'success_url' => null, diff --git a/tests/Unit/Api/Command/PayByCardHandlerTest.php b/tests/Unit/Api/Command/PayByCardHandlerTest.php index 9e256ed8..f39501f1 100644 --- a/tests/Unit/Api/Command/PayByCardHandlerTest.php +++ b/tests/Unit/Api/Command/PayByCardHandlerTest.php @@ -66,7 +66,7 @@ public function test_it_creates_a_card_based_transaction_with_saving_card_option $payment = $this->prophesize(PaymentInterface::class); $payment->getDetails()->willReturn([], ['tpay' => ['status' => 'pending', 'payment_url' => 'https://cw.org/pay']]); $payment->setDetails( - $this->getExpectedDetails(card: 'encoded_card_data', saveCreditCardForLater: true), + $this->getExpectedDetails(card: 'encoded_card_data', save_credit_card_for_later: true), )->shouldBeCalled(); $this->paymentRepository->find(1)->willReturn($payment); diff --git a/tests/Unit/Payum/Action/Api/PayWithCardActionTest.php b/tests/Unit/Payum/Action/Api/PayWithCardActionTest.php index ea3518e4..7970c958 100644 --- a/tests/Unit/Payum/Action/Api/PayWithCardActionTest.php +++ b/tests/Unit/Payum/Action/Api/PayWithCardActionTest.php @@ -4,12 +4,15 @@ namespace Tests\CommerceWeavers\SyliusTpayPlugin\Unit\Payum\Action\Api; +use CommerceWeavers\SyliusTpayPlugin\Model\PaymentDetails; use CommerceWeavers\SyliusTpayPlugin\Payum\Action\Api\PayWithCardAction; +use CommerceWeavers\SyliusTpayPlugin\Payum\Mapper\PayWithCardActionPayloadMapperInterface; use CommerceWeavers\SyliusTpayPlugin\Payum\Request\Api\PayWithCard; use CommerceWeavers\SyliusTpayPlugin\Tpay\TpayApi; use Payum\Core\Reply\HttpRedirect; use Payum\Core\Security\TokenInterface; use PHPUnit\Framework\TestCase; +use Prophecy\Argument; use Prophecy\PhpUnit\ProphecyTrait; use Prophecy\Prophecy\ObjectProphecy; use Sylius\Component\Core\Model\OrderInterface; @@ -25,10 +28,12 @@ final class PayWithCardActionTest extends TestCase use PaymentDetailsHelperTrait; private TpayApi|ObjectProphecy $api; + private PayWithCardActionPayloadMapperInterface|ObjectProphecy $mapper; protected function setUp(): void { $this->api = $this->prophesize(TpayApi::class); + $this->mapper = $this->prophesize(PayWithCardActionPayloadMapperInterface::class); } public function test_it_supports_pay_with_card_request_with_a_payment_model(): void @@ -90,13 +95,10 @@ public function test_it_redirects_a_customer_to_3ds_verification_once_a_transact $request->getModel()->willReturn($paymentModel->reveal()); $request->getToken()->willReturn($token->reveal()); + $this->mapper->getPayload(Argument::type(PaymentDetails::class))->willReturn(['GENERATED' => 'PAYLOAD']); + $transactions = $this->prophesize(TransactionsApi::class); - $transactions->createPaymentByTransactionId([ - 'groupId' => 103, - 'cardPaymentData' => [ - 'card' => $details['tpay']['card'], - ], - ], $details['tpay']['transaction_id'])->willReturn($response); + $transactions->createPaymentByTransactionId(['GENERATED' => 'PAYLOAD'], $details['tpay']['transaction_id'])->willReturn($response); $this->api->transactions()->willReturn($transactions); @@ -133,12 +135,9 @@ public function test_it_marks_payment_as_failed_if_tpay_throws_an_exception(): v $request->getToken()->willReturn($token->reveal()); $transactions = $this->prophesize(TransactionsApi::class); - $transactions->createPaymentByTransactionId([ - 'groupId' => 103, - 'cardPaymentData' => [ - 'card' => $details['tpay']['card'], - ], - ], $details['tpay']['transaction_id'])->willThrow(new TpayException('Something went wrong')); + + $this->mapper->getPayload(Argument::type(PaymentDetails::class))->willReturn(['GENERATED' => 'PAYLOAD']); + $transactions->createPaymentByTransactionId(['GENERATED' => 'PAYLOAD'], $details['tpay']['transaction_id'])->willThrow(new TpayException('Something went wrong')); $this->api->transactions()->willReturn($transactions); @@ -151,63 +150,6 @@ public function test_it_marks_payment_as_failed_if_tpay_throws_an_exception(): v $subject->execute($request->reveal()); } - public function test_it_redirects_a_customer_to_3ds_verification_and_save_a_card(): void - { - $this->expectException(HttpRedirect::class); - - $request = $this->prophesize(PayWithCard::class); - $paymentModel = $this->prophesize(PaymentInterface::class); - $details = [ - 'tpay' => [ - 'card' => 'test-card', - 'transaction_id' => 'abcd', - 'saveCreditCardForLater' => true, - ], - ]; - - $response = [ - 'result' => 'success', - 'status' => 'pending', - 'transactionPaymentUrl' => 'http://example.com', - ]; - - $request->getModel()->willReturn($paymentModel->reveal()); - $paymentModel->getDetails()->willReturn($details); - - $transactions = $this->prophesize(TransactionsApi::class); - $transactions->createPaymentByTransactionId([ - 'groupId' => 103, - 'cardPaymentData' => [ - 'card' => $details['tpay']['card'], - 'save' => true, - ], - ], $details['tpay']['transaction_id'])->willReturn($response); - - $this->api->transactions()->willReturn($transactions); - - $paymentModel->setDetails([ - 'tpay' => [ - 'transaction_id' => 'abcd', - 'result' => 'success', - 'status' => 'pending', - 'apple_pay_token' => null, - 'blik_token' => null, - 'google_pay_token' => null, - 'card' => null, - 'saveCreditCardForLater' => true, - 'payment_url' => 'http://example.com', - 'success_url' => null, - 'failure_url' => null, - 'tpay_channel_id' => null, - 'visa_mobile_phone_number' => null, - ], - ])->shouldBeCalled(); - - $subject = $this->createTestSubject(); - - $subject->execute($request->reveal()); - } - public function test_it_marks_a_payment_status_as_failed_once_a_transaction_status_is_failed(): void { $details = [ @@ -238,13 +180,10 @@ public function test_it_marks_a_payment_status_as_failed_once_a_transaction_stat 'result' => 'failed', ]; + $this->mapper->getPayload(Argument::type(PaymentDetails::class))->willReturn(['GENERATED' => 'PAYLOAD']); + $transactions = $this->prophesize(TransactionsApi::class); - $transactions->createPaymentByTransactionId([ - 'groupId' => 103, - 'cardPaymentData' => [ - 'card' => $details['tpay']['card'], - ], - ], $details['tpay']['transaction_id'])->willReturn($response); + $transactions->createPaymentByTransactionId(['GENERATED' => 'PAYLOAD'], $details['tpay']['transaction_id'])->willReturn($response); $this->api->transactions()->willReturn($transactions); @@ -256,7 +195,7 @@ public function test_it_marks_a_payment_status_as_failed_once_a_transaction_stat private function createTestSubject(): PayWithCardAction { - $action = new PayWithCardAction(); + $action = new PayWithCardAction($this->mapper->reveal()); $action->setApi($this->api->reveal()); diff --git a/tests/Unit/Payum/Action/Api/SaveCreditCardActionTest.php b/tests/Unit/Payum/Action/Api/SaveCreditCardActionTest.php index b9059ffb..70286254 100644 --- a/tests/Unit/Payum/Action/Api/SaveCreditCardActionTest.php +++ b/tests/Unit/Payum/Action/Api/SaveCreditCardActionTest.php @@ -13,6 +13,7 @@ use Payum\Core\Request\Sync; use Payum\Core\Security\TokenInterface; use PHPUnit\Framework\TestCase; +use Prophecy\Argument; use Prophecy\PhpUnit\ProphecyTrait; use Prophecy\Prophecy\ObjectProphecy; use Sylius\Component\Core\Model\CustomerInterface; @@ -87,6 +88,7 @@ public function test_it_saves_returned_credit_card(): void $this->model->setDetails($this->getExpectedDetails())->shouldBeCalled(); + $creditCard->setUid(Argument::type('string'))->shouldBeCalled(); $creditCard->setTail('card_tail')->shouldBeCalled(); $creditCard->setBrand('card_brand')->shouldBeCalled(); $creditCard->setToken('card_token')->shouldBeCalled(); From e31f4135f76524d8126b094ca2ffe37c453f013b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=81ukasz=20Chru=C5=9Bciel?= Date: Wed, 30 Oct 2024 19:28:42 +0100 Subject: [PATCH 12/20] Add support for paying by saved card via API --- config/serialization/Pay.xml | 3 + src/Api/Command/Pay.php | 1 + src/Api/Command/PayBySavedCard.php | 14 ++++ src/Api/Command/PayBySavedCardHandler.php | 31 +++++++ .../PayByCardAndSavedCardFactory.php | 27 +++++++ .../NextCommand/PayBySavedCardFactory.php | 33 ++++++++ .../Api/Command/PayBySavedCardHandlerTest.php | 70 ++++++++++++++++ .../PayByCardAndSavedCardFactoryTest.php | 59 ++++++++++++++ .../NextCommand/PayBySavedCardFactoryTest.php | 80 +++++++++++++++++++ 9 files changed, 318 insertions(+) create mode 100644 src/Api/Command/PayBySavedCard.php create mode 100644 src/Api/Command/PayBySavedCardHandler.php create mode 100644 src/Api/Factory/NextCommand/PayByCardAndSavedCardFactory.php create mode 100644 src/Api/Factory/NextCommand/PayBySavedCardFactory.php create mode 100644 tests/Unit/Api/Command/PayBySavedCardHandlerTest.php create mode 100644 tests/Unit/Api/Factory/NextCommand/PayByCardAndSavedCardFactoryTest.php create mode 100644 tests/Unit/Api/Factory/NextCommand/PayBySavedCardFactoryTest.php diff --git a/config/serialization/Pay.xml b/config/serialization/Pay.xml index 9c1f1e38..bbf75fbf 100644 --- a/config/serialization/Pay.xml +++ b/config/serialization/Pay.xml @@ -32,6 +32,9 @@ commerce_weavers_sylius_tpay:shop:order:pay + + commerce_weavers_sylius_tpay:shop:order:pay + commerce_weavers_sylius_tpay:shop:order:pay diff --git a/src/Api/Command/Pay.php b/src/Api/Command/Pay.php index 130f656f..7bc69fed 100644 --- a/src/Api/Command/Pay.php +++ b/src/Api/Command/Pay.php @@ -19,6 +19,7 @@ public function __construct( public readonly ?string $blikAliasApplicationCode = null, public readonly ?string $googlePayToken = null, public readonly ?string $encodedCardData = null, + public readonly ?int $savedCardId = null, public readonly bool $saveCard = false, public readonly ?string $tpayChannelId = null, public readonly ?string $visaMobilePhoneNumber = null, diff --git a/src/Api/Command/PayBySavedCard.php b/src/Api/Command/PayBySavedCard.php new file mode 100644 index 00000000..185ede85 --- /dev/null +++ b/src/Api/Command/PayBySavedCard.php @@ -0,0 +1,14 @@ +findOr404($command->paymentId); + + $this->setTransactionData($payment, $command->savedCardId); + $this->createTransactionProcessor->process($payment); + + return $this->createResultFrom($payment); + } + + private function setTransactionData(PaymentInterface $payment, int $savedCardId, bool $saveCard = false): void + { + $paymentDetails = PaymentDetails::fromArray($payment->getDetails()); + $paymentDetails->setUseSavedCreditCard($savedCardId); + + $payment->setDetails($paymentDetails->toArray()); + } +} diff --git a/src/Api/Factory/NextCommand/PayByCardAndSavedCardFactory.php b/src/Api/Factory/NextCommand/PayByCardAndSavedCardFactory.php new file mode 100644 index 00000000..4a7054a4 --- /dev/null +++ b/src/Api/Factory/NextCommand/PayByCardAndSavedCardFactory.php @@ -0,0 +1,27 @@ +supports($command, $payment)) { + throw new UnsupportedNextCommandFactory('This factory does not support the given command.'); + } + + throw new UnsupportedNextCommandFactory('Saved card UID and encoded card data cannot be used together.'); + } + + public function supports(Pay $command, PaymentInterface $payment): bool + { + return $command->encodedCardData !== null && $command->savedCardId !== null && $payment->getId() !== null; + } +} diff --git a/src/Api/Factory/NextCommand/PayBySavedCardFactory.php b/src/Api/Factory/NextCommand/PayBySavedCardFactory.php new file mode 100644 index 00000000..1935bb03 --- /dev/null +++ b/src/Api/Factory/NextCommand/PayBySavedCardFactory.php @@ -0,0 +1,33 @@ +supports($command, $payment)) { + throw new UnsupportedNextCommandFactory('This factory does not support the given command.'); + } + + /** @var int $paymentId */ + $paymentId = $payment->getId(); + /** @var int $savedCardId */ + $savedCardId = $command->savedCardId; + + return new PayBySavedCard($paymentId, $savedCardId); + } + + public function supports(Pay $command, PaymentInterface $payment): bool + { + return $command->savedCardId !== null && $payment->getId() !== null; + } +} diff --git a/tests/Unit/Api/Command/PayBySavedCardHandlerTest.php b/tests/Unit/Api/Command/PayBySavedCardHandlerTest.php new file mode 100644 index 00000000..93d96346 --- /dev/null +++ b/tests/Unit/Api/Command/PayBySavedCardHandlerTest.php @@ -0,0 +1,70 @@ +paymentRepository = $this->prophesize(PaymentRepositoryInterface::class); + $this->createTransactionProcessor = $this->prophesize(CreateTransactionProcessorInterface::class); + } + + public function test_it_throw_an_exception_if_a_payment_cannot_be_found(): void + { + $this->expectException(NotFoundHttpException::class); + $this->expectExceptionMessage('Payment with id "1" cannot be found.'); + + $this->paymentRepository->find(1)->willReturn(null); + + $this->createTestSubject()->__invoke(new PayBySavedCard(1, 1)); + } + + public function test_it_creates_a_card_based_transaction(): void + { + $payment = $this->prophesize(PaymentInterface::class); + $payment->getDetails()->willReturn([], ['tpay' => ['status' => 'pending', 'payment_url' => 'https://cw.org/pay']]); + $payment->setDetails( + $this->getExpectedDetails(use_saved_credit_card: 1), + )->shouldBeCalled(); + + $this->paymentRepository->find(1)->willReturn($payment); + + $result = $this->createTestSubject()->__invoke(new PayBySavedCard(1, 1)); + + $this->assertSame('pending', $result->status); + $this->assertSame('https://cw.org/pay', $result->transactionPaymentUrl); + } + + private function createTestSubject(): PayBySavedCardHandler + { + return new PayBySavedCardHandler( + $this->paymentRepository->reveal(), + $this->createTransactionProcessor->reveal(), + ); + } +} diff --git a/tests/Unit/Api/Factory/NextCommand/PayByCardAndSavedCardFactoryTest.php b/tests/Unit/Api/Factory/NextCommand/PayByCardAndSavedCardFactoryTest.php new file mode 100644 index 00000000..ae9b9984 --- /dev/null +++ b/tests/Unit/Api/Factory/NextCommand/PayByCardAndSavedCardFactoryTest.php @@ -0,0 +1,59 @@ +createTestSubject(); + + $this->assertTrue($factory->supports($this->createCommand(), $this->createPayment())); + } + + public function test_it_throws_an_exception_when_trying_to_create_a_command_with_unsupported_factory(): void + { + $this->expectException(UnsupportedNextCommandFactory::class); + + $this->createTestSubject()->create($this->createCommand(), new Payment()); + } + + private function createCommand(?string $token = null): Pay + { + return new Pay( + $token ?? 'token', + 'https://cw.nonexisting/success', + 'https://cw.nonexisting/failure', + encodedCardData: 'card_data', + savedCardId: 1, + ); + } + + private function createPayment(int $id = 1): PaymentInterface + { + $payment = $this->prophesize(PaymentInterface::class); + $payment->getId()->willReturn($id); + + return $payment->reveal(); + } + + private function createTestSubject(): NextCommandFactoryInterface + { + return new PayByCardAndSavedCardFactory(); + } +} diff --git a/tests/Unit/Api/Factory/NextCommand/PayBySavedCardFactoryTest.php b/tests/Unit/Api/Factory/NextCommand/PayBySavedCardFactoryTest.php new file mode 100644 index 00000000..0eb5d899 --- /dev/null +++ b/tests/Unit/Api/Factory/NextCommand/PayBySavedCardFactoryTest.php @@ -0,0 +1,80 @@ +createTestSubject(); + + $this->assertFalse($factory->supports($this->createCommand(), $this->createPayment())); + } + + public function test_it_does_not_support_a_command_without_a_payment_with_id(): void + { + $factory = $this->createTestSubject(); + + $this->assertFalse($factory->supports($this->createCommand(savedCardId: 1), new Payment())); + } + + public function test_it_supports_a_command_with_an_saved_card_data(): void + { + $factory = $this->createTestSubject(); + + $this->assertTrue($factory->supports($this->createCommand(savedCardId: 1), $this->createPayment())); + } + + public function test_it_creates_a_pay_by_saved_card_command(): void + { + $command = $this->createTestSubject()->create($this->createCommand(savedCardId: 1), $this->createPayment()); + + $this->assertInstanceOf(PayBySavedCard::class, $command); + $this->assertSame(1, $command->savedCardId); + } + + public function test_it_throws_an_exception_when_trying_to_create_a_command_with_unsupported_factory(): void + { + $this->expectException(UnsupportedNextCommandFactory::class); + + $this->createTestSubject()->create($this->createCommand(), new Payment()); + } + + private function createCommand(?string $token = null, ?int $savedCardId = null, bool $saveCard = false): Pay + { + return new Pay( + $token ?? 'token', + 'https://cw.nonexisting/success', + 'https://cw.nonexisting/failure', + savedCardId: $savedCardId, + saveCard: $saveCard, + ); + } + + private function createPayment(int $id = 1): PaymentInterface + { + $payment = $this->prophesize(PaymentInterface::class); + $payment->getId()->willReturn($id); + + return $payment->reveal(); + } + + private function createTestSubject(): NextCommandFactoryInterface + { + return new PayBySavedCardFactory(); + } +} From 90d60bf229d52097450d9900a097b1502c064f46 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=81ukasz=20Chru=C5=9Bciel?= Date: Wed, 30 Oct 2024 19:49:58 +0100 Subject: [PATCH 13/20] Remove VarDumper --- src/Form/EventListener/AddSavedCreditCardsListener.php | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/Form/EventListener/AddSavedCreditCardsListener.php b/src/Form/EventListener/AddSavedCreditCardsListener.php index 50630db7..bda613e9 100644 --- a/src/Form/EventListener/AddSavedCreditCardsListener.php +++ b/src/Form/EventListener/AddSavedCreditCardsListener.php @@ -74,8 +74,6 @@ public function __invoke(FormEvent $event): void $choices[$stringifiedCard] = $creditCard->getId(); } - VarDumper::dump($choices); - $form ->add( 'useSavedCreditCard', From 81279ae851b88f53896f671a26eb286aada63ed4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=81ukasz=20Chru=C5=9Bciel?= Date: Wed, 30 Oct 2024 21:31:27 +0100 Subject: [PATCH 14/20] Fix support for saved card payments over API --- config/serialization/Pay.xml | 2 +- config/services/api/command.php | 6 ++ config/services/api/factory.php | 10 ++ config/validation/Pay.xml | 24 ++++- src/Api/Command/Pay.php | 2 +- src/Api/Command/PayBySavedCardHandler.php | 3 +- .../Factory/NextCommand/PayByCardFactory.php | 2 +- .../AddSavedCreditCardsListener.php | 1 - src/Model/PaymentDetails.php | 1 + src/Payum/Action/Api/PayWithCardAction.php | 4 +- .../shop/paying_for_orders_by_card/cart.yml | 2 +- .../paying_for_orders_by_card/credit_card.yml | 8 ++ tests/Api/Shop/PayingForOrdersByCardTest.php | 91 ++++++++++++------- translations/validators.en.yaml | 2 + translations/validators.pl.yaml | 2 + 15 files changed, 118 insertions(+), 42 deletions(-) create mode 100644 tests/Api/DataFixtures/shop/paying_for_orders_by_card/credit_card.yml diff --git a/config/serialization/Pay.xml b/config/serialization/Pay.xml index bbf75fbf..65676fd2 100644 --- a/config/serialization/Pay.xml +++ b/config/serialization/Pay.xml @@ -32,7 +32,7 @@ commerce_weavers_sylius_tpay:shop:order:pay - + commerce_weavers_sylius_tpay:shop:order:pay diff --git a/config/services/api/command.php b/config/services/api/command.php index 3cc49926..d95b81af 100644 --- a/config/services/api/command.php +++ b/config/services/api/command.php @@ -12,6 +12,7 @@ use CommerceWeavers\SyliusTpayPlugin\Api\Command\PayByGooglePayHandler; use CommerceWeavers\SyliusTpayPlugin\Api\Command\PayByLinkHandler; use CommerceWeavers\SyliusTpayPlugin\Api\Command\PayByRedirectHandler; +use CommerceWeavers\SyliusTpayPlugin\Api\Command\PayBySavedCardHandler; use CommerceWeavers\SyliusTpayPlugin\Api\Command\PayByVisaMobileHandler; use CommerceWeavers\SyliusTpayPlugin\Api\Command\PayHandler; use CommerceWeavers\SyliusTpayPlugin\Command\CancelLastPaymentHandler; @@ -75,6 +76,11 @@ ->tag('messenger.message_handler') ; + $services->set('commerce_weavers_sylius_tpay.api.command.pay_by_saved_card_handler', PayBySavedCardHandler::class) + ->parent('commerce_weavers_sylius_tpay.api.command.abstract_pay_by_handler') + ->tag('messenger.message_handler') + ; + $services->set('commerce_weavers_sylius_tpay.api.command.pay_by_google_pay_handler', PayByGooglePayHandler::class) ->parent('commerce_weavers_sylius_tpay.api.command.abstract_pay_by_handler') ->tag('messenger.message_handler') diff --git a/config/services/api/factory.php b/config/services/api/factory.php index dab15e3d..209a7df2 100644 --- a/config/services/api/factory.php +++ b/config/services/api/factory.php @@ -6,10 +6,12 @@ use CommerceWeavers\SyliusTpayPlugin\Api\Factory\NextCommand\PayByApplePayFactory; use CommerceWeavers\SyliusTpayPlugin\Api\Factory\NextCommand\PayByBlikFactory; +use CommerceWeavers\SyliusTpayPlugin\Api\Factory\NextCommand\PayByCardAndSavedCardFactory; use CommerceWeavers\SyliusTpayPlugin\Api\Factory\NextCommand\PayByCardFactory; use CommerceWeavers\SyliusTpayPlugin\Api\Factory\NextCommand\PayByGooglePayFactory; use CommerceWeavers\SyliusTpayPlugin\Api\Factory\NextCommand\PayByLinkFactory; use CommerceWeavers\SyliusTpayPlugin\Api\Factory\NextCommand\PayByRedirectFactory; +use CommerceWeavers\SyliusTpayPlugin\Api\Factory\NextCommand\PayBySavedCardFactory; use CommerceWeavers\SyliusTpayPlugin\Api\Factory\NextCommand\PayByVisaMobileFactory; use CommerceWeavers\SyliusTpayPlugin\Api\Factory\NextCommandFactory; use CommerceWeavers\SyliusTpayPlugin\Api\Factory\NextCommandFactoryInterface; @@ -36,6 +38,14 @@ ->tag('commerce_weavers_sylius_tpay.api.factory.next_command') ; + $services->set('commerce_weavers_sylius_tpay.api.factory.next_command.pay_by_saved_card', PayBySavedCardFactory::class) + ->tag('commerce_weavers_sylius_tpay.api.factory.next_command') + ; + + $services->set('commerce_weavers_sylius_tpay.api.factory.next_command.pay_by_card_and_saved_card', PayByCardAndSavedCardFactory::class) + ->tag('commerce_weavers_sylius_tpay.api.factory.next_command') + ; + $services->set('commerce_weavers_sylius_tpay.api.factory.next_command.pay_by_google_pay', PayByGooglePayFactory::class) ->tag('commerce_weavers_sylius_tpay.api.factory.next_command') ; diff --git a/config/validation/Pay.xml b/config/validation/Pay.xml index dee002c9..707ca5df 100644 --- a/config/validation/Pay.xml +++ b/config/validation/Pay.xml @@ -12,6 +12,17 @@ commerce_weavers_sylius_tpay:shop:order:pay + + + + + + @@ -58,10 +69,15 @@ - - - - + + + + + + + diff --git a/src/Api/Command/Pay.php b/src/Api/Command/Pay.php index 7bc69fed..9f24d489 100644 --- a/src/Api/Command/Pay.php +++ b/src/Api/Command/Pay.php @@ -20,7 +20,7 @@ public function __construct( public readonly ?string $googlePayToken = null, public readonly ?string $encodedCardData = null, public readonly ?int $savedCardId = null, - public readonly bool $saveCard = false, + public readonly ?bool $saveCard = null, public readonly ?string $tpayChannelId = null, public readonly ?string $visaMobilePhoneNumber = null, ) { diff --git a/src/Api/Command/PayBySavedCardHandler.php b/src/Api/Command/PayBySavedCardHandler.php index dd821425..e5200458 100644 --- a/src/Api/Command/PayBySavedCardHandler.php +++ b/src/Api/Command/PayBySavedCardHandler.php @@ -16,12 +16,13 @@ public function __invoke(PayBySavedCard $command): PayResult $payment = $this->findOr404($command->paymentId); $this->setTransactionData($payment, $command->savedCardId); + $this->createTransactionProcessor->process($payment); return $this->createResultFrom($payment); } - private function setTransactionData(PaymentInterface $payment, int $savedCardId, bool $saveCard = false): void + private function setTransactionData(PaymentInterface $payment, int $savedCardId): void { $paymentDetails = PaymentDetails::fromArray($payment->getDetails()); $paymentDetails->setUseSavedCreditCard($savedCardId); diff --git a/src/Api/Factory/NextCommand/PayByCardFactory.php b/src/Api/Factory/NextCommand/PayByCardFactory.php index a5df13b3..f9a90155 100644 --- a/src/Api/Factory/NextCommand/PayByCardFactory.php +++ b/src/Api/Factory/NextCommand/PayByCardFactory.php @@ -25,7 +25,7 @@ public function create(Pay $command, PaymentInterface $payment): PayByCard $saveCard = $command->saveCard; - return new PayByCard($paymentId, $encodedCardData, $saveCard); + return new PayByCard($paymentId, $encodedCardData, $saveCard ?? false); } public function supports(Pay $command, PaymentInterface $payment): bool diff --git a/src/Form/EventListener/AddSavedCreditCardsListener.php b/src/Form/EventListener/AddSavedCreditCardsListener.php index bda613e9..def3769c 100644 --- a/src/Form/EventListener/AddSavedCreditCardsListener.php +++ b/src/Form/EventListener/AddSavedCreditCardsListener.php @@ -14,7 +14,6 @@ use Symfony\Component\Form\FormInterface; use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface; use Symfony\Component\Translation\TranslatableMessage; -use Symfony\Component\VarDumper\VarDumper; use Symfony\Contracts\Translation\TranslatorInterface; final class AddSavedCreditCardsListener diff --git a/src/Model/PaymentDetails.php b/src/Model/PaymentDetails.php index b5167a62..866e2074 100644 --- a/src/Model/PaymentDetails.php +++ b/src/Model/PaymentDetails.php @@ -201,6 +201,7 @@ public function getType(): string { return match (true) { null !== $this->getEncodedCardData() => PaymentType::CARD, + null !== $this->getUseSavedCreditCard() => PaymentType::CARD, null !== $this->getBlikToken() => PaymentType::BLIK, null !== $this->getTpayChannelId() => PaymentType::PAY_BY_LINK, null !== $this->getGooglePayToken() => PaymentType::GOOGLE_PAY, diff --git a/src/Payum/Action/Api/PayWithCardAction.php b/src/Payum/Action/Api/PayWithCardAction.php index 1e858d0c..592581ea 100644 --- a/src/Payum/Action/Api/PayWithCardAction.php +++ b/src/Payum/Action/Api/PayWithCardAction.php @@ -21,7 +21,9 @@ public function __construct(private readonly PayWithCardActionPayloadMapperInter protected function doExecute(Generic $request, PaymentInterface $model, PaymentDetails $paymentDetails, string $gatewayName, string $localeCode): void { - Assert::notNull($paymentDetails->getEncodedCardData(), 'Card data is required to pay with card.'); + if ($paymentDetails->getEncodedCardData() === null && $paymentDetails->getUseSavedCreditCard() === null) { + throw new \InvalidArgumentException('Card data is required to pay with card.'); + } Assert::notNull($paymentDetails->getTransactionId(), 'Transaction ID is required to pay with card.'); $this->do( diff --git a/tests/Api/DataFixtures/shop/paying_for_orders_by_card/cart.yml b/tests/Api/DataFixtures/shop/paying_for_orders_by_card/cart.yml index 8895a962..33ca5ae2 100644 --- a/tests/Api/DataFixtures/shop/paying_for_orders_by_card/cart.yml +++ b/tests/Api/DataFixtures/shop/paying_for_orders_by_card/cart.yml @@ -4,7 +4,7 @@ Sylius\Component\Core\Model\Customer: lastName: 'Doe' email: 'sylius@example.com' emailCanonical: 'sylius@example.com' - + Sylius\Component\Core\Model\ShopUser: user_john_doe: plainPassword: '123pa\\$\\$word' diff --git a/tests/Api/DataFixtures/shop/paying_for_orders_by_card/credit_card.yml b/tests/Api/DataFixtures/shop/paying_for_orders_by_card/credit_card.yml new file mode 100644 index 00000000..0d1fc97d --- /dev/null +++ b/tests/Api/DataFixtures/shop/paying_for_orders_by_card/credit_card.yml @@ -0,0 +1,8 @@ +CommerceWeavers\SyliusTpayPlugin\Entity\CreditCard: + single_credit_card: + brand: 'Visa' + expirationDate: "<(new \\DateTime('+1 year'))>" + tail: '1234' + token: '8d1bb66340e35c112117459231057592b3afc0a5253e3e2d171a72da4921ffc5' + customer: '@customer_john_doe' + channel: '@channel_web' diff --git a/tests/Api/Shop/PayingForOrdersByCardTest.php b/tests/Api/Shop/PayingForOrdersByCardTest.php index 18d5f5f6..6c82d059 100644 --- a/tests/Api/Shop/PayingForOrdersByCardTest.php +++ b/tests/Api/Shop/PayingForOrdersByCardTest.php @@ -53,6 +53,57 @@ public function test_paying_with_a_valid_encrypted_card_data_for_an_order(): voi $this->assertResponse($response, 'shop/paying_for_orders_by_card/test_paying_with_a_valid_card_for_an_order'); } + public function test_paying_with_a_saved_card_data_for_an_order(): void + { + $loadFixtures = $this->loadFixturesFromDirectory('shop/paying_for_orders_by_card'); + + $order = $this->doPlaceOrder('t0k3n', paymentMethodCode: 'tpay_card'); + + $authorizationHeader = $this->logInUser('shop', self::FIXTURE_EMAIL); + + $this->client->request( + Request::METHOD_POST, + sprintf('/api/v2/shop/orders/%s/pay', $order->getTokenValue()), + server: self::CONTENT_TYPE_HEADER + $authorizationHeader, + content: json_encode([ + 'successUrl' => 'https://example.com/success', + 'failureUrl' => 'https://example.com/failure', + 'savedCardId' => $loadFixtures['single_credit_card']->getId(), + ]), + ); + + $response = $this->client->getResponse(); + + $this->assertResponseCode($response, Response::HTTP_OK); + $this->assertResponse($response, 'shop/paying_for_orders_by_card/test_paying_with_a_valid_card_for_an_order'); + } + + public function test_trying_paying_with_a_saved_without_being_logged_in(): void + { + $loadFixtures = $this->loadFixturesFromDirectory('shop/paying_for_orders_by_card'); + + $order = $this->doPlaceOrder('t0k3n', paymentMethodCode: 'tpay_card'); + + $this->client->request( + Request::METHOD_POST, + sprintf('/api/v2/shop/orders/%s/pay', $order->getTokenValue()), + server: self::CONTENT_TYPE_HEADER, + content: json_encode([ + 'successUrl' => 'https://example.com/success', + 'failureUrl' => 'https://example.com/failure', + 'savedCardId' => $loadFixtures['single_credit_card']->getId(), + ]), + ); + + $response = $this->client->getResponse(); + + $this->assertResponseCode($response, 422); + $this->assertStringContainsString( + 'savedCardId: You are not authorized to perform this action."', + $response->getContent(), + ); + } + public function test_it_handles_tpay_error_while_paying_with_card_based_payment_type(): void { $this->loadFixturesFromDirectory('shop/paying_for_orders_by_card'); @@ -134,9 +185,9 @@ public function test_trying_saving_cart_without_being_logged_in(): void $response = $this->client->getResponse(); - $this->assertResponseCode($response, 424); + $this->assertResponseCode($response, 422); $this->assertStringContainsString( - 'An error occurred while processing your payment. Please try again or contact store support.', + 'saveCard: You are not authorized to perform this action."', $response->getContent(), ); } @@ -161,8 +212,8 @@ public function test_paying_without_a_card_data_when_a_tpay_card_payment_has_bee $this->assertResponseViolations($response, [ [ - 'propertyPath' => 'encodedCardData', - 'message' => 'The card data is required.', + 'propertyPath' => '', + 'message' => 'You must provide new card data or an identifier of a saved card.', ] ]); } @@ -178,32 +229,10 @@ public static function data_provider_paying_without_a_card_data_when_a_tpay_card 'failureUrl' => 'https://example.com/failure', 'blikToken' => '777123', ]]; - } - - public function test_paying_with_providing_an_empty_card_data(): void - { - $this->loadFixturesFromDirectory('shop/paying_for_orders_by_card'); - - $order = $this->doPlaceOrder('t0k3n', paymentMethodCode: 'tpay_card'); - - $this->client->request( - Request::METHOD_POST, - sprintf('/api/v2/shop/orders/%s/pay', $order->getTokenValue()), - server: self::CONTENT_TYPE_HEADER, - content: json_encode([ - 'successUrl' => 'https://example.com/success', - 'failureUrl' => 'https://example.com/failure', - 'encodedCardData' => '', - ]), - ); - - $response = $this->client->getResponse(); - - $this->assertResponseViolations($response, [ - [ - 'propertyPath' => 'encodedCardData', - 'message' => 'The card data is required.', - ] - ]); + yield 'content with a empty card data' => [[ + 'successUrl' => 'https://example.com/success', + 'failureUrl' => 'https://example.com/failure', + 'encodedCardData' => '', + ]]; } } diff --git a/translations/validators.en.yaml b/translations/validators.en.yaml index d17c14ea..e1ed8b28 100644 --- a/translations/validators.en.yaml +++ b/translations/validators.en.yaml @@ -11,6 +11,7 @@ commerce_weavers_sylius_tpay: required_with_alias_register: 'The BLIK token is required with an alias register action.' field: not_blank: 'This value should not be blank.' + user_not_authorized: 'You are not authorized to perform this action.' google_pay_token: required: 'The Google Pay token is required.' not_json_encoded: 'The Google Pay token must be a JSON object encoded with Base64.' @@ -23,6 +24,7 @@ commerce_weavers_sylius_tpay: not_available: 'Channel with provided id is not available.' not_a_bank: 'Channel with provided id is not a bank.' card: + required_fields: 'You must provide new card data or an identifier of a saved card.' cvc: 'The CVC must be composed of 3 digits.' expiration_month: 'The expiration month must be the current month or later.' expiration_year: 'The expiration year must be the current year or later.' diff --git a/translations/validators.pl.yaml b/translations/validators.pl.yaml index 71b1775d..01cd66df 100644 --- a/translations/validators.pl.yaml +++ b/translations/validators.pl.yaml @@ -11,6 +11,7 @@ commerce_weavers_sylius_tpay: required_with_alias_register: 'Kod BLIK jest wymagany w przypadku rejestracji aliasu Blik.' field: not_blank: 'Ta wartość nie powinna być pusta.' + user_not_authorized: 'Nie masz uprawnień do wykonania tej akcji.' google_pay_token: required: 'Token Google Pay jest wymagany.' not_json_encoded: 'Token Google Pay musi być obiektem JSON zakodowanym przez Base64.' @@ -23,6 +24,7 @@ commerce_weavers_sylius_tpay: not_available: 'Kanał z podanym id nie jest dostępny.' not_a_bank: 'Kanał z podanym id nie jest bankiem.' card: + required_fields: 'Musisz podać dane nowej karty bądź identyfikator już zapamiętanej karty.' cvc: 'Kod CVC musi składać się z 3 cyfr.' expiration_month: 'Miesiąc ważności musi być bieżący lub późniejszy.' expiration_year: 'Rok ważności musi być bieżący lub późniejszy.' From 3447b2f9786c3e7e8dcef683503f62f77eddb33f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=81ukasz=20Chru=C5=9Bciel?= Date: Wed, 30 Oct 2024 22:54:31 +0100 Subject: [PATCH 15/20] Add support for UUID for CreditCard --- config/api_resources/credit_card.yaml | 15 ++++++++-- config/doctrine/CreditCard.orm.xml | 4 +-- config/routes_shop.php | 1 + config/serialization/CreditCard.xml | 25 ++++++++++++++++ migrations/Version20241029160137.php | 2 ++ ...30113142.php => Version20241030214842.php} | 5 ++-- src/Api/Command/Pay.php | 2 +- src/Api/Command/PayBySavedCard.php | 2 +- src/Api/Command/PayBySavedCardHandler.php | 2 +- .../NextCommand/PayBySavedCardFactory.php | 2 +- src/DependencyInjection/Configuration.php | 3 +- src/Entity/CreditCard.php | 18 +++--------- src/Entity/CreditCardInterface.php | 4 --- src/Factory/CreditCardFactory.php | 29 +++++++++++++++++++ src/Factory/CreditCardFactoryInterface.php | 11 +++++++ .../AddSavedCreditCardsListener.php | 1 + src/Model/PaymentDetails.php | 6 ++-- src/Payum/Action/Api/SaveCreditCardAction.php | 2 -- .../paying_for_orders_by_card/credit_card.yml | 1 + tests/Api/Shop/PayingForOrdersByCardTest.php | 4 +-- .../Api/Command/PayBySavedCardHandlerTest.php | 6 ++-- .../PayByCardAndSavedCardFactoryTest.php | 2 +- .../NextCommand/PayBySavedCardFactoryTest.php | 10 +++---- .../Action/Api/SaveCreditCardActionTest.php | 2 -- 24 files changed, 109 insertions(+), 50 deletions(-) create mode 100644 config/serialization/CreditCard.xml rename migrations/{Version20241030113142.php => Version20241030214842.php} (65%) create mode 100644 src/Factory/CreditCardFactory.php create mode 100644 src/Factory/CreditCardFactoryInterface.php diff --git a/config/api_resources/credit_card.yaml b/config/api_resources/credit_card.yaml index 212d4caa..7e18f820 100644 --- a/config/api_resources/credit_card.yaml +++ b/config/api_resources/credit_card.yaml @@ -1,11 +1,20 @@ '%commerce_weavers_sylius_tpay.model.credit_card.class%': collectionOperations: - get: + shop_get: + method: 'GET' path: /shop/credit-cards + normalization_context: + groups: + - 'commerce_weavers_sylius_tpay:shop:credit_card:index' itemOperations: - get: + shop_get: + method: 'GET' path: /shop/credit-cards/{id} - delete: + normalization_context: + groups: + - 'commerce_weavers_sylius_tpay:shop:credit_card:show' + shop_delete: + method: 'DELETE' path: /shop/credit-cards/{id} properties: id: diff --git a/config/doctrine/CreditCard.orm.xml b/config/doctrine/CreditCard.orm.xml index 6edca3ba..1bc06ce0 100644 --- a/config/doctrine/CreditCard.orm.xml +++ b/config/doctrine/CreditCard.orm.xml @@ -6,9 +6,7 @@ xsi:schemaLocation="http://doctrine-project.org/schemas/orm/doctrine-mapping http://doctrine-project.org/schemas/orm/doctrine-mapping.xsd" > - - - + diff --git a/config/routes_shop.php b/config/routes_shop.php index 7cef0a2f..50e7d833 100644 --- a/config/routes_shop.php +++ b/config/routes_shop.php @@ -42,6 +42,7 @@ $routes->add(Routing::SHOP_ACCOUNT_CREDIT_CARD_DELETE, Routing::SHOP_ACCOUNT_CREDIT_CARD_DELETE_PATH) ->controller('commerce_weavers_sylius_tpay.controller.credit_card::deleteAction') ->methods([Request::METHOD_DELETE]) + ->requirements(['id' => '[0-9a-f]{8}-[0-9a-f]{4}-[1-6][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}']) ->defaults([ '_sylius' => [ 'section' => 'shop_account', diff --git a/config/serialization/CreditCard.xml b/config/serialization/CreditCard.xml new file mode 100644 index 00000000..deeade58 --- /dev/null +++ b/config/serialization/CreditCard.xml @@ -0,0 +1,25 @@ + + + + + + commerce_weavers_sylius_tpay:shop:credit_card:index + commerce_weavers_sylius_tpay:shop:credit_card:show + + + commerce_weavers_sylius_tpay:shop:credit_card:index + commerce_weavers_sylius_tpay:shop:credit_card:show + + + commerce_weavers_sylius_tpay:shop:credit_card:index + commerce_weavers_sylius_tpay:shop:credit_card:show + + + commerce_weavers_sylius_tpay:shop:credit_card:index + commerce_weavers_sylius_tpay:shop:credit_card:show + + + diff --git a/migrations/Version20241029160137.php b/migrations/Version20241029160137.php index c0432658..f4cf71f2 100644 --- a/migrations/Version20241029160137.php +++ b/migrations/Version20241029160137.php @@ -21,6 +21,7 @@ public function up(Schema $schema): void $this->addSql('ALTER TABLE cw_sylius_tpay_credt_card ADD channel_id INT NOT NULL'); $this->addSql('ALTER TABLE cw_sylius_tpay_credt_card ADD CONSTRAINT FK_9FF1996C72F5A1AA FOREIGN KEY (channel_id) REFERENCES sylius_channel (id) ON DELETE CASCADE'); $this->addSql('CREATE INDEX IDX_9FF1996C72F5A1AA ON cw_sylius_tpay_credt_card (channel_id)'); + $this->addSql('ALTER TABLE cw_sylius_tpay_credt_card CHANGE id id VARCHAR(255) NOT NULL'); } public function down(Schema $schema): void @@ -30,5 +31,6 @@ public function down(Schema $schema): void $this->addSql('ALTER TABLE cw_sylius_tpay_credt_card DROP FOREIGN KEY FK_9FF1996C72F5A1AA'); $this->addSql('DROP INDEX IDX_9FF1996C72F5A1AA ON cw_sylius_tpay_credt_card'); $this->addSql('ALTER TABLE cw_sylius_tpay_credt_card DROP channel_id'); + $this->addSql('ALTER TABLE cw_sylius_tpay_credt_card CHANGE id id INT AUTO_INCREMENT NOT NULL'); } } diff --git a/migrations/Version20241030113142.php b/migrations/Version20241030214842.php similarity index 65% rename from migrations/Version20241030113142.php rename to migrations/Version20241030214842.php index 881bab72..d7a1aed0 100644 --- a/migrations/Version20241030113142.php +++ b/migrations/Version20241030214842.php @@ -10,7 +10,7 @@ /** * Auto-generated Migration: Please modify to your needs! */ -final class Version20241030113142 extends AbstractMigration +final class Version20241030214842 extends AbstractMigration { public function getDescription(): string { @@ -20,12 +20,11 @@ public function getDescription(): string public function up(Schema $schema): void { // this up() migration is auto-generated, please modify it to your needs - $this->addSql('ALTER TABLE cw_sylius_tpay_blik_alias CHANGE registered registered TINYINT(1) DEFAULT false NOT NULL'); } public function down(Schema $schema): void { // this down() migration is auto-generated, please modify it to your needs - $this->addSql('ALTER TABLE cw_sylius_tpay_blik_alias CHANGE registered registered TINYINT(1) DEFAULT 0 NOT NULL'); + $this->addSql('ALTER TABLE cw_sylius_tpay_credt_card CHANGE id id BINARY(16) NOT NULL COMMENT \'(DC2Type:uuid)\''); } } diff --git a/src/Api/Command/Pay.php b/src/Api/Command/Pay.php index 9f24d489..246c1ebf 100644 --- a/src/Api/Command/Pay.php +++ b/src/Api/Command/Pay.php @@ -19,7 +19,7 @@ public function __construct( public readonly ?string $blikAliasApplicationCode = null, public readonly ?string $googlePayToken = null, public readonly ?string $encodedCardData = null, - public readonly ?int $savedCardId = null, + public readonly ?string $savedCardId = null, public readonly ?bool $saveCard = null, public readonly ?string $tpayChannelId = null, public readonly ?string $visaMobilePhoneNumber = null, diff --git a/src/Api/Command/PayBySavedCard.php b/src/Api/Command/PayBySavedCard.php index 185ede85..d10ac09f 100644 --- a/src/Api/Command/PayBySavedCard.php +++ b/src/Api/Command/PayBySavedCard.php @@ -8,7 +8,7 @@ final class PayBySavedCard { public function __construct( public readonly int $paymentId, - public readonly int $savedCardId, + public readonly string $savedCardId, ) { } } diff --git a/src/Api/Command/PayBySavedCardHandler.php b/src/Api/Command/PayBySavedCardHandler.php index e5200458..91510072 100644 --- a/src/Api/Command/PayBySavedCardHandler.php +++ b/src/Api/Command/PayBySavedCardHandler.php @@ -22,7 +22,7 @@ public function __invoke(PayBySavedCard $command): PayResult return $this->createResultFrom($payment); } - private function setTransactionData(PaymentInterface $payment, int $savedCardId): void + private function setTransactionData(PaymentInterface $payment, string $savedCardId): void { $paymentDetails = PaymentDetails::fromArray($payment->getDetails()); $paymentDetails->setUseSavedCreditCard($savedCardId); diff --git a/src/Api/Factory/NextCommand/PayBySavedCardFactory.php b/src/Api/Factory/NextCommand/PayBySavedCardFactory.php index 1935bb03..fd6a3510 100644 --- a/src/Api/Factory/NextCommand/PayBySavedCardFactory.php +++ b/src/Api/Factory/NextCommand/PayBySavedCardFactory.php @@ -20,7 +20,7 @@ public function create(Pay $command, PaymentInterface $payment): PayBySavedCard /** @var int $paymentId */ $paymentId = $payment->getId(); - /** @var int $savedCardId */ + /** @var string $savedCardId */ $savedCardId = $command->savedCardId; return new PayBySavedCard($paymentId, $savedCardId); diff --git a/src/DependencyInjection/Configuration.php b/src/DependencyInjection/Configuration.php index d44f5b6b..257fa5cf 100644 --- a/src/DependencyInjection/Configuration.php +++ b/src/DependencyInjection/Configuration.php @@ -9,6 +9,7 @@ use CommerceWeavers\SyliusTpayPlugin\Entity\CreditCard; use CommerceWeavers\SyliusTpayPlugin\Entity\CreditCardInterface; use CommerceWeavers\SyliusTpayPlugin\Factory\BlikAliasFactory; +use CommerceWeavers\SyliusTpayPlugin\Factory\CreditCardFactory; use CommerceWeavers\SyliusTpayPlugin\Repository\BlikAliasRepository; use CommerceWeavers\SyliusTpayPlugin\Repository\CreditCardRepository; use Sylius\Bundle\ResourceBundle\Controller\ResourceController; @@ -62,7 +63,7 @@ private function addResourcesSection(ArrayNodeDefinition $node): void ->scalarNode('model')->defaultValue(CreditCard::class)->cannotBeEmpty()->end() ->scalarNode('interface')->defaultValue(CreditCardInterface::class)->cannotBeEmpty()->end() ->scalarNode('controller')->defaultValue(ResourceController::class)->cannotBeEmpty()->end() - ->scalarNode('factory')->defaultValue(Factory::class)->cannotBeEmpty()->end() + ->scalarNode('factory')->defaultValue(CreditCardFactory::class)->cannotBeEmpty()->end() ->scalarNode('repository')->defaultValue(CreditCardRepository::class)->cannotBeEmpty()->end() ->end() ->end() diff --git a/src/Entity/CreditCard.php b/src/Entity/CreditCard.php index b304b6f2..381b4bbe 100644 --- a/src/Entity/CreditCard.php +++ b/src/Entity/CreditCard.php @@ -9,9 +9,9 @@ class CreditCard implements CreditCardInterface { - private ?int $id = null; - - private ?string $uid = null; + public function __construct(private ?string $id = null) + { + } private ?string $token = null; @@ -25,21 +25,11 @@ class CreditCard implements CreditCardInterface private ?ChannelInterface $channel = null; - public function getId(): ?int + public function getId(): ?string { return $this->id; } - public function getUid(): ?string - { - return $this->uid; - } - - public function setUid(?string $uid): void - { - $this->uid = $uid; - } - public function getToken(): ?string { return $this->token; diff --git a/src/Entity/CreditCardInterface.php b/src/Entity/CreditCardInterface.php index 6c46c691..9c3f2f4c 100644 --- a/src/Entity/CreditCardInterface.php +++ b/src/Entity/CreditCardInterface.php @@ -9,10 +9,6 @@ interface CreditCardInterface extends ResourceInterface { - public function getUid(): ?string; - - public function setUid(?string $uid): void; - public function getToken(): ?string; public function setToken(?string $token): void; diff --git a/src/Factory/CreditCardFactory.php b/src/Factory/CreditCardFactory.php new file mode 100644 index 00000000..cedae64f --- /dev/null +++ b/src/Factory/CreditCardFactory.php @@ -0,0 +1,29 @@ + $className + */ + public function __construct(private readonly string $className) + { + if (!is_a($className, CreditCardInterface::class, true)) { + throw new \DomainException(sprintf( + 'This factory requires %s or its descend to be used as resource', + CreditCardInterface::class, + )); + } + } + + public function createNew(): CreditCardInterface + { + return new $this->className(Uuid::v4()->toRfc4122()); + } +} diff --git a/src/Factory/CreditCardFactoryInterface.php b/src/Factory/CreditCardFactoryInterface.php new file mode 100644 index 00000000..5f1ac85d --- /dev/null +++ b/src/Factory/CreditCardFactoryInterface.php @@ -0,0 +1,11 @@ +saveCreditCardForLater = $saveCreditCardForLater; } - public function getUseSavedCreditCard(): ?int + public function getUseSavedCreditCard(): ?string { return $this->useSavedCreditCard; } - public function setUseSavedCreditCard(?int $useSavedCreditCard): void + public function setUseSavedCreditCard(?string $useSavedCreditCard): void { $this->useSavedCreditCard = $useSavedCreditCard; } diff --git a/src/Payum/Action/Api/SaveCreditCardAction.php b/src/Payum/Action/Api/SaveCreditCardAction.php index bfc028a1..f8d73b46 100644 --- a/src/Payum/Action/Api/SaveCreditCardAction.php +++ b/src/Payum/Action/Api/SaveCreditCardAction.php @@ -15,7 +15,6 @@ use Sylius\Component\Core\Model\PaymentInterface; use Sylius\Component\Resource\Repository\RepositoryInterface; use Sylius\Resource\Factory\FactoryInterface; -use Symfony\Component\Uid\Uuid; use Webmozart\Assert\Assert; final class SaveCreditCardAction extends BasePaymentAwareAction implements GatewayAwareInterface @@ -37,7 +36,6 @@ protected function doExecute(Generic $request, PaymentInterface $model, PaymentD /** @var CreditCardInterface $creditCard */ $creditCard = $this->creditCardFactory->createNew(); - $creditCard->setUid(Uuid::v4()->toRfc4122()); $creditCard->setToken($request->getCardToken()); $creditCard->setBrand($request->getCardBrand()); $creditCard->setTail($request->getCardTail()); diff --git a/tests/Api/DataFixtures/shop/paying_for_orders_by_card/credit_card.yml b/tests/Api/DataFixtures/shop/paying_for_orders_by_card/credit_card.yml index 0d1fc97d..b98f523a 100644 --- a/tests/Api/DataFixtures/shop/paying_for_orders_by_card/credit_card.yml +++ b/tests/Api/DataFixtures/shop/paying_for_orders_by_card/credit_card.yml @@ -1,5 +1,6 @@ CommerceWeavers\SyliusTpayPlugin\Entity\CreditCard: single_credit_card: + __construct: ['e0f79275-18ef-4edf-b8fc-adc40fdcbcc0'] brand: 'Visa' expirationDate: "<(new \\DateTime('+1 year'))>" tail: '1234' diff --git a/tests/Api/Shop/PayingForOrdersByCardTest.php b/tests/Api/Shop/PayingForOrdersByCardTest.php index 6c82d059..c160bdce 100644 --- a/tests/Api/Shop/PayingForOrdersByCardTest.php +++ b/tests/Api/Shop/PayingForOrdersByCardTest.php @@ -55,7 +55,7 @@ public function test_paying_with_a_valid_encrypted_card_data_for_an_order(): voi public function test_paying_with_a_saved_card_data_for_an_order(): void { - $loadFixtures = $this->loadFixturesFromDirectory('shop/paying_for_orders_by_card'); + $this->loadFixturesFromDirectory('shop/paying_for_orders_by_card'); $order = $this->doPlaceOrder('t0k3n', paymentMethodCode: 'tpay_card'); @@ -68,7 +68,7 @@ public function test_paying_with_a_saved_card_data_for_an_order(): void content: json_encode([ 'successUrl' => 'https://example.com/success', 'failureUrl' => 'https://example.com/failure', - 'savedCardId' => $loadFixtures['single_credit_card']->getId(), + 'savedCardId' => 'e0f79275-18ef-4edf-b8fc-adc40fdcbcc0', ]), ); diff --git a/tests/Unit/Api/Command/PayBySavedCardHandlerTest.php b/tests/Unit/Api/Command/PayBySavedCardHandlerTest.php index 93d96346..834b2e8d 100644 --- a/tests/Unit/Api/Command/PayBySavedCardHandlerTest.php +++ b/tests/Unit/Api/Command/PayBySavedCardHandlerTest.php @@ -41,7 +41,7 @@ public function test_it_throw_an_exception_if_a_payment_cannot_be_found(): void $this->paymentRepository->find(1)->willReturn(null); - $this->createTestSubject()->__invoke(new PayBySavedCard(1, 1)); + $this->createTestSubject()->__invoke(new PayBySavedCard(1, 'e0f79275-18ef-4edf-b8fc-adc40fdcbcc0')); } public function test_it_creates_a_card_based_transaction(): void @@ -49,12 +49,12 @@ public function test_it_creates_a_card_based_transaction(): void $payment = $this->prophesize(PaymentInterface::class); $payment->getDetails()->willReturn([], ['tpay' => ['status' => 'pending', 'payment_url' => 'https://cw.org/pay']]); $payment->setDetails( - $this->getExpectedDetails(use_saved_credit_card: 1), + $this->getExpectedDetails(use_saved_credit_card: 'e0f79275-18ef-4edf-b8fc-adc40fdcbcc0'), )->shouldBeCalled(); $this->paymentRepository->find(1)->willReturn($payment); - $result = $this->createTestSubject()->__invoke(new PayBySavedCard(1, 1)); + $result = $this->createTestSubject()->__invoke(new PayBySavedCard(1, 'e0f79275-18ef-4edf-b8fc-adc40fdcbcc0')); $this->assertSame('pending', $result->status); $this->assertSame('https://cw.org/pay', $result->transactionPaymentUrl); diff --git a/tests/Unit/Api/Factory/NextCommand/PayByCardAndSavedCardFactoryTest.php b/tests/Unit/Api/Factory/NextCommand/PayByCardAndSavedCardFactoryTest.php index ae9b9984..6a8149d8 100644 --- a/tests/Unit/Api/Factory/NextCommand/PayByCardAndSavedCardFactoryTest.php +++ b/tests/Unit/Api/Factory/NextCommand/PayByCardAndSavedCardFactoryTest.php @@ -40,7 +40,7 @@ private function createCommand(?string $token = null): Pay 'https://cw.nonexisting/success', 'https://cw.nonexisting/failure', encodedCardData: 'card_data', - savedCardId: 1, + savedCardId: 'e0f79275-18ef-4edf-b8fc-adc40fdcbcc0', ); } diff --git a/tests/Unit/Api/Factory/NextCommand/PayBySavedCardFactoryTest.php b/tests/Unit/Api/Factory/NextCommand/PayBySavedCardFactoryTest.php index 0eb5d899..3e9664eb 100644 --- a/tests/Unit/Api/Factory/NextCommand/PayBySavedCardFactoryTest.php +++ b/tests/Unit/Api/Factory/NextCommand/PayBySavedCardFactoryTest.php @@ -29,22 +29,22 @@ public function test_it_does_not_support_a_command_without_a_payment_with_id(): { $factory = $this->createTestSubject(); - $this->assertFalse($factory->supports($this->createCommand(savedCardId: 1), new Payment())); + $this->assertFalse($factory->supports($this->createCommand(savedCardId: 'e0f79275-18ef-4edf-b8fc-adc40fdcbcc0'), new Payment())); } public function test_it_supports_a_command_with_an_saved_card_data(): void { $factory = $this->createTestSubject(); - $this->assertTrue($factory->supports($this->createCommand(savedCardId: 1), $this->createPayment())); + $this->assertTrue($factory->supports($this->createCommand(savedCardId: 'e0f79275-18ef-4edf-b8fc-adc40fdcbcc0'), $this->createPayment())); } public function test_it_creates_a_pay_by_saved_card_command(): void { - $command = $this->createTestSubject()->create($this->createCommand(savedCardId: 1), $this->createPayment()); + $command = $this->createTestSubject()->create($this->createCommand(savedCardId: 'e0f79275-18ef-4edf-b8fc-adc40fdcbcc0'), $this->createPayment()); $this->assertInstanceOf(PayBySavedCard::class, $command); - $this->assertSame(1, $command->savedCardId); + $this->assertSame('e0f79275-18ef-4edf-b8fc-adc40fdcbcc0', $command->savedCardId); } public function test_it_throws_an_exception_when_trying_to_create_a_command_with_unsupported_factory(): void @@ -54,7 +54,7 @@ public function test_it_throws_an_exception_when_trying_to_create_a_command_with $this->createTestSubject()->create($this->createCommand(), new Payment()); } - private function createCommand(?string $token = null, ?int $savedCardId = null, bool $saveCard = false): Pay + private function createCommand(?string $token = null, ?string $savedCardId = null, bool $saveCard = false): Pay { return new Pay( $token ?? 'token', diff --git a/tests/Unit/Payum/Action/Api/SaveCreditCardActionTest.php b/tests/Unit/Payum/Action/Api/SaveCreditCardActionTest.php index 70286254..b9059ffb 100644 --- a/tests/Unit/Payum/Action/Api/SaveCreditCardActionTest.php +++ b/tests/Unit/Payum/Action/Api/SaveCreditCardActionTest.php @@ -13,7 +13,6 @@ use Payum\Core\Request\Sync; use Payum\Core\Security\TokenInterface; use PHPUnit\Framework\TestCase; -use Prophecy\Argument; use Prophecy\PhpUnit\ProphecyTrait; use Prophecy\Prophecy\ObjectProphecy; use Sylius\Component\Core\Model\CustomerInterface; @@ -88,7 +87,6 @@ public function test_it_saves_returned_credit_card(): void $this->model->setDetails($this->getExpectedDetails())->shouldBeCalled(); - $creditCard->setUid(Argument::type('string'))->shouldBeCalled(); $creditCard->setTail('card_tail')->shouldBeCalled(); $creditCard->setBrand('card_brand')->shouldBeCalled(); $creditCard->setToken('card_token')->shouldBeCalled(); From a3600c4b445bdca558796fb4877567edee30894f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=81ukasz=20Chru=C5=9Bciel?= Date: Wed, 30 Oct 2024 23:28:58 +0100 Subject: [PATCH 16/20] Bring back thank you page --- config/routes_shop.php | 5 +++++ src/DependencyInjection/Configuration.php | 1 - src/Factory/CreditCardFactoryInterface.php | 2 ++ src/Form/EventListener/AddSavedCreditCardsListener.php | 1 - 4 files changed, 7 insertions(+), 2 deletions(-) diff --git a/config/routes_shop.php b/config/routes_shop.php index 50e7d833..2afd7c71 100644 --- a/config/routes_shop.php +++ b/config/routes_shop.php @@ -27,6 +27,11 @@ ->methods([Request::METHOD_GET]) ; + $routes->add(Routing::SHOP_WAITING_FOR_PAYMENT, Routing::SHOP_WAITING_FOR_PAYMENT_PATH) + ->controller(DisplayWaitingForPaymentPage::class) + ->methods([Request::METHOD_GET]) + ; + $routes->add(Routing::SHOP_ACCOUNT_CREDIT_CARD_INDEX, Routing::SHOP_ACCOUNT_CREDIT_CARD_INDEX_PATH) ->controller('commerce_weavers_sylius_tpay.controller.credit_card::indexAction') ->methods([Request::METHOD_GET]) diff --git a/src/DependencyInjection/Configuration.php b/src/DependencyInjection/Configuration.php index 257fa5cf..c6a4381b 100644 --- a/src/DependencyInjection/Configuration.php +++ b/src/DependencyInjection/Configuration.php @@ -13,7 +13,6 @@ use CommerceWeavers\SyliusTpayPlugin\Repository\BlikAliasRepository; use CommerceWeavers\SyliusTpayPlugin\Repository\CreditCardRepository; use Sylius\Bundle\ResourceBundle\Controller\ResourceController; -use Sylius\Resource\Factory\Factory; use Symfony\Component\Config\Definition\Builder\ArrayNodeDefinition; use Symfony\Component\Config\Definition\Builder\TreeBuilder; use Symfony\Component\Config\Definition\ConfigurationInterface; diff --git a/src/Factory/CreditCardFactoryInterface.php b/src/Factory/CreditCardFactoryInterface.php index 5f1ac85d..21bd2c42 100644 --- a/src/Factory/CreditCardFactoryInterface.php +++ b/src/Factory/CreditCardFactoryInterface.php @@ -1,5 +1,7 @@ Date: Thu, 31 Oct 2024 00:24:55 +0100 Subject: [PATCH 17/20] Improve fields definitions and fix flow --- config/services/form.php | 4 +- src/Entity/CreditCardInterface.php | 5 ++ .../AddSavedCreditCardsListener.php | 1 + src/Form/Type/TpayPaymentDetailsType.php | 59 ++++++++++++++++--- src/Model/PaymentDetails.php | 3 +- src/Payum/Action/Api/SaveCreditCardAction.php | 1 + templates/shop/payment/_card.html.twig | 10 ++-- .../Action/Api/SaveCreditCardActionTest.php | 7 +++ 8 files changed, 75 insertions(+), 15 deletions(-) diff --git a/config/services/form.php b/config/services/form.php index e7f67ebd..c1831e4a 100644 --- a/config/services/form.php +++ b/config/services/form.php @@ -51,8 +51,10 @@ $services->set(TpayPaymentDetailsType::class) ->args([ service('commerce_weavers_sylius_tpay.form.event_listener.remove_unnecessary_payment_details_fields'), - service('commerce_weavers_sylius_tpay.form.event_listener.add_saved_credit_cards'), service('security.token_storage'), + service('translator'), + service('commerce_weavers_sylius_tpay.repository.credit_card'), + service('sylius.context.cart'), ]) ->tag('form.type') ; diff --git a/src/Entity/CreditCardInterface.php b/src/Entity/CreditCardInterface.php index 9c3f2f4c..9ec2543f 100644 --- a/src/Entity/CreditCardInterface.php +++ b/src/Entity/CreditCardInterface.php @@ -4,6 +4,7 @@ namespace CommerceWeavers\SyliusTpayPlugin\Entity; +use Sylius\Component\Core\Model\ChannelInterface; use Sylius\Component\Core\Model\CustomerInterface; use Sylius\Resource\Model\ResourceInterface; @@ -28,4 +29,8 @@ public function setExpirationDate(?\DateTimeInterface $expirationDate): void; public function getCustomer(): ?CustomerInterface; public function setCustomer(?CustomerInterface $customer): void; + + public function getChannel(): ?ChannelInterface; + + public function setChannel(?ChannelInterface $channel): void; } diff --git a/src/Form/EventListener/AddSavedCreditCardsListener.php b/src/Form/EventListener/AddSavedCreditCardsListener.php index def3769c..3ffa180a 100644 --- a/src/Form/EventListener/AddSavedCreditCardsListener.php +++ b/src/Form/EventListener/AddSavedCreditCardsListener.php @@ -28,6 +28,7 @@ public function __construct( public function __invoke(FormEvent $event): void { $form = $event->getForm(); + /** @var FormInterface $form */ $form = $form->getParent(); diff --git a/src/Form/Type/TpayPaymentDetailsType.php b/src/Form/Type/TpayPaymentDetailsType.php index af50841b..e52a2568 100644 --- a/src/Form/Type/TpayPaymentDetailsType.php +++ b/src/Form/Type/TpayPaymentDetailsType.php @@ -4,25 +4,34 @@ namespace CommerceWeavers\SyliusTpayPlugin\Form\Type; +use CommerceWeavers\SyliusTpayPlugin\Repository\CreditCardRepositoryInterface; use CommerceWeavers\SyliusTpayPlugin\Validator\Constraint\EncodedGooglePayToken; +use Sylius\Component\Core\Model\CustomerInterface; +use Sylius\Component\Core\Model\OrderInterface; use Sylius\Component\Core\Model\ShopUserInterface; +use Sylius\Component\Order\Context\CartContextInterface; use Symfony\Component\Form\AbstractType; use Symfony\Component\Form\Extension\Core\Type\CheckboxType; +use Symfony\Component\Form\Extension\Core\Type\ChoiceType; use Symfony\Component\Form\Extension\Core\Type\HiddenType; use Symfony\Component\Form\Extension\Core\Type\TelType; use Symfony\Component\Form\Extension\Core\Type\TextType; use Symfony\Component\Form\FormBuilderInterface; use Symfony\Component\Form\FormEvents; use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface; +use Symfony\Component\Translation\TranslatableMessage; use Symfony\Component\Validator\Constraints\Length; use Symfony\Component\Validator\Constraints\Regex; +use Symfony\Contracts\Translation\TranslatorInterface; final class TpayPaymentDetailsType extends AbstractType { public function __construct( private readonly object $removeUnnecessaryPaymentDetailsFieldsListener, - private readonly object $addSavedCreditCardsListener, private readonly TokenStorageInterface $tokenStorage, + private readonly TranslatorInterface $translator, + private readonly CreditCardRepositoryInterface $creditCardRepository, + private readonly CartContextInterface $cartContext, ) { } @@ -107,24 +116,60 @@ public function buildForm(FormBuilderInterface $builder, array $options): void [$this->removeUnnecessaryPaymentDetailsFieldsListener, '__invoke'], ); - $builder->addEventListener( - FormEvents::PRE_SET_DATA, - [$this->addSavedCreditCardsListener, '__invoke'], - ); - $token = $this->tokenStorage->getToken(); $user = $token?->getUser(); if ($user instanceof ShopUserInterface) { $builder ->add( - 'saveCreditCardForLater', + 'save_credit_card_for_later', CheckboxType::class, [ 'label' => 'commerce_weavers_sylius_tpay.shop.order_summary.card.save_credit_card_for_later.label', ], ) ; + + /** @var OrderInterface $order */ + $order = $this->cartContext->getCart(); + $channel = $order->getChannel(); + /** @var CustomerInterface $customer */ + $customer = $user->getCustomer(); + + $creditCards = $this->creditCardRepository->findByCustomerAndChannel($customer, $channel); + + if (count($creditCards) === 0) { + return; + } + + $choices = []; + + foreach ($creditCards as $creditCard) { + $stringifiedCard = $this->translator->trans( + 'commerce_weavers_sylius_tpay.shop.credit_card.card_selection_one_liner', + [ + '%brand%' => $creditCard->getBrand(), + '%tail%' => $creditCard->getTail(), + '%expires%' => $creditCard->getExpirationDate()->format('m-Y'), + ], + 'messages', + ); + + $choices[$stringifiedCard] = $creditCard->getId(); + } + + $builder + ->add( + 'use_saved_credit_card', + ChoiceType::class, + [ + 'label' => 'commerce_weavers_sylius_tpay.shop.order_summary.card.use_saved_credit_card.label', + 'placeholder' => new TranslatableMessage('commerce_weavers_sylius_tpay.shop.credit_card.use_new_card'), + 'required' => false, + 'choices' => $choices, + ], + ) + ; } } } diff --git a/src/Model/PaymentDetails.php b/src/Model/PaymentDetails.php index 88251add..e404f25b 100644 --- a/src/Model/PaymentDetails.php +++ b/src/Model/PaymentDetails.php @@ -200,8 +200,7 @@ public function setTpayChannelId(?string $tpayChannelId): void public function getType(): string { return match (true) { - null !== $this->getEncodedCardData() => PaymentType::CARD, - null !== $this->getUseSavedCreditCard() => PaymentType::CARD, + null !== $this->getEncodedCardData(), null !== $this->getUseSavedCreditCard() => PaymentType::CARD, null !== $this->getBlikToken() => PaymentType::BLIK, null !== $this->getTpayChannelId() => PaymentType::PAY_BY_LINK, null !== $this->getGooglePayToken() => PaymentType::GOOGLE_PAY, diff --git a/src/Payum/Action/Api/SaveCreditCardAction.php b/src/Payum/Action/Api/SaveCreditCardAction.php index f8d73b46..1b3f4094 100644 --- a/src/Payum/Action/Api/SaveCreditCardAction.php +++ b/src/Payum/Action/Api/SaveCreditCardAction.php @@ -47,6 +47,7 @@ protected function doExecute(Generic $request, PaymentInterface $model, PaymentD Assert::isInstanceOf($customer, CustomerInterface::class); $creditCard->setCustomer($customer); + $creditCard->setChannel($order?->getChannel()); $expiryDate = $request->getTokenExpiryDate(); diff --git a/templates/shop/payment/_card.html.twig b/templates/shop/payment/_card.html.twig index 40172dae..e6d96148 100644 --- a/templates/shop/payment/_card.html.twig +++ b/templates/shop/payment/_card.html.twig @@ -2,12 +2,12 @@
- {% if form.tpay.useSavedCreditCard is defined %} + {% if form.tpay.use_saved_credit_card is defined %}
- {{ form_label(form.tpay.useSavedCreditCard) }} + {{ form_label(form.tpay.use_saved_credit_card) }}
- {{ form_widget(form.tpay.useSavedCreditCard, { attr: {'data-tpay-saved-card': ''}}) }} + {{ form_widget(form.tpay.use_saved_credit_card, { attr: {'data-tpay-saved-card': ''}}) }}
@@ -56,9 +56,9 @@
- {% if form.tpay.saveCreditCardForLater is defined %} + {% if form.tpay.save_credit_card_for_later is defined %}
- {{ form_row(form.tpay.saveCreditCardForLater) }} + {{ form_row(form.tpay.save_credit_card_for_later) }}
{% endif %} diff --git a/tests/Unit/Payum/Action/Api/SaveCreditCardActionTest.php b/tests/Unit/Payum/Action/Api/SaveCreditCardActionTest.php index b9059ffb..43eeb6dd 100644 --- a/tests/Unit/Payum/Action/Api/SaveCreditCardActionTest.php +++ b/tests/Unit/Payum/Action/Api/SaveCreditCardActionTest.php @@ -15,6 +15,7 @@ use PHPUnit\Framework\TestCase; use Prophecy\PhpUnit\ProphecyTrait; use Prophecy\Prophecy\ObjectProphecy; +use Sylius\Component\Core\Model\ChannelInterface; use Sylius\Component\Core\Model\CustomerInterface; use Sylius\Component\Core\Model\OrderInterface; use Sylius\Component\Core\Model\PaymentInterface; @@ -34,6 +35,8 @@ final class SaveCreditCardActionTest extends TestCase private CustomerInterface|ObjectProphecy $customer; + private ChannelInterface|ObjectProphecy $channel; + private TpayApi|ObjectProphecy $api; private BasicPaymentFactoryInterface|ObjectProphecy $factory; @@ -53,6 +56,9 @@ protected function setUp(): void $this->customer = $this->prophesize(CustomerInterface::class); $order->getCustomer()->willReturn($this->customer->reveal()); + $this->channel = $this->prophesize(ChannelInterface::class); + $order->getChannel()->willReturn($this->channel->reveal()); + $this->model = $this->prophesize(PaymentInterface::class); $this->model->getOrder()->willReturn($order->reveal()); $this->model->getDetails()->willReturn([]); @@ -92,6 +98,7 @@ public function test_it_saves_returned_credit_card(): void $creditCard->setToken('card_token')->shouldBeCalled(); $creditCard->setExpirationDate(new \DateTimeImmutable('01-11-2028'))->shouldBeCalled(); $creditCard->setCustomer($this->customer)->shouldBeCalled(); + $creditCard->setChannel($this->channel)->shouldBeCalled(); $this->repository->add($creditCard->reveal())->shouldBeCalled(); From 5b582d94ef5a98a4d72be7fe61adac7502509d72 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=81ukasz=20Chru=C5=9Bciel?= Date: Thu, 31 Oct 2024 11:06:28 +0100 Subject: [PATCH 18/20] Improve end-to-end tests --- .../Checkout/TpayCreditCardCheckoutTest.php | 18 +++--------------- tests/E2E/Helper/Order/TpayTrait.php | 2 +- 2 files changed, 4 insertions(+), 16 deletions(-) diff --git a/tests/E2E/Checkout/TpayCreditCardCheckoutTest.php b/tests/E2E/Checkout/TpayCreditCardCheckoutTest.php index 85b66e8a..dc8b97b5 100644 --- a/tests/E2E/Checkout/TpayCreditCardCheckoutTest.php +++ b/tests/E2E/Checkout/TpayCreditCardCheckoutTest.php @@ -26,38 +26,26 @@ protected function setUp(): void $this->loadFixtures(['addressed_cart.yaml']); - // the cart is already addressed, so we go straight to selecting a shipping method + $this->loginShopUser('tony@nonexisting.cw', 'sylius'); $this->showSelectingShippingMethodStep(); $this->processWithDefaultShippingMethod(); } public function test_it_completes_the_checkout_using_credit_card(): void { - $this->loginShopUser('tony@nonexisting.cw', 'sylius'); - $this->processWithPaymentMethod('tpay_card'); $this->fillCardData(self::FORM_ID, self::CARD_NUMBER, '123', '01', '2029'); $this->placeOrder(); - $this->assertPageTitleContains('Thank you!'); + $this->assertPageTitleContains('Waiting for payment'); } public function test_it_completes_the_checkout_using_credit_card_and_saves_the_card(): void { - $this->loginShopUser('tony@nonexisting.cw', 'sylius'); - $this->processWithPaymentMethod('tpay_card'); $this->fillCardData(self::FORM_ID, self::CARD_NUMBER, '123', '01', '2029', true); $this->placeOrder(); - $this->assertPageTitleContains('Thank you!'); - } - - public function test_it_forbids_card_saving_for_not_logged_in_users(): void - { - $this->expectException(NoSuchElementException::class); - - $this->processWithPaymentMethod('tpay_card'); - $this->fillCardData(self::FORM_ID, self::CARD_NUMBER, '123', '01', '2029', true); + $this->assertPageTitleContains('Waiting for payment'); } } diff --git a/tests/E2E/Helper/Order/TpayTrait.php b/tests/E2E/Helper/Order/TpayTrait.php index 443ece71..a06e1dde 100644 --- a/tests/E2E/Helper/Order/TpayTrait.php +++ b/tests/E2E/Helper/Order/TpayTrait.php @@ -21,7 +21,7 @@ public function fillCardData(string $formId, string $cardNumber, string $cvv, st $this->client->findElement(WebDriverBy::id(sprintf('%s_tpay_card_expiration_date_year', $formId)))->sendKeys($year); if ($saveCardForLater) { - $this->client->findElement(WebDriverBy::id(sprintf('%s_tpay_saveCreditCardForLater', $formId)))->click(); + $this->client->findElement(WebDriverBy::id(sprintf('%s_tpay_save_credit_card_for_later', $formId)))->sendKeys(true); } } From 540b22470f262f1b7edf4964b3d55cbe13cc453d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=81ukasz=20Chru=C5=9Bciel?= Date: Thu, 31 Oct 2024 11:22:44 +0100 Subject: [PATCH 19/20] Remove obsolate migration, adjust grid and fix small things --- config/api_resources/credit_card.yaml | 4 +++ config/config.php | 1 - .../{credit_card.yml => credit_card.yaml} | 0 config/services/payum/action.php | 2 +- migrations/Version20241030214842.php | 30 ------------------- 5 files changed, 5 insertions(+), 32 deletions(-) rename config/grid/{credit_card.yml => credit_card.yaml} (100%) delete mode 100644 migrations/Version20241030214842.php diff --git a/config/api_resources/credit_card.yaml b/config/api_resources/credit_card.yaml index 7e18f820..11a48a89 100644 --- a/config/api_resources/credit_card.yaml +++ b/config/api_resources/credit_card.yaml @@ -19,3 +19,7 @@ properties: id: identifier: true + tail: + writable: false + brand: + writable: false diff --git a/config/config.php b/config/config.php index 1cdfe31c..f201835e 100644 --- a/config/config.php +++ b/config/config.php @@ -8,7 +8,6 @@ return static function(ContainerConfigurator $container): void { $container->import('config/**/*.php'); - $container->import('grid/*.yml'); $container->import('grid/*.yaml'); $parameters = $container->parameters(); diff --git a/config/grid/credit_card.yml b/config/grid/credit_card.yaml similarity index 100% rename from config/grid/credit_card.yml rename to config/grid/credit_card.yaml diff --git a/config/services/payum/action.php b/config/services/payum/action.php index fdf2a72e..41103d30 100644 --- a/config/services/payum/action.php +++ b/config/services/payum/action.php @@ -99,7 +99,7 @@ service('commerce_weavers_sylius_tpay.factory.credit_card'), service('commerce_weavers_sylius_tpay.repository.credit_card'), ]) - ->tag('payum.action', ['factory' => TpayGatewayFactory::NAME, 'alias' => 'cw.tpay.credit_card']) + ->tag('payum.action', ['factory' => TpayGatewayFactory::NAME, 'alias' => 'cw.tpay.save_credit_card']) ; $services->set(PayWithCardAction::class) diff --git a/migrations/Version20241030214842.php b/migrations/Version20241030214842.php deleted file mode 100644 index d7a1aed0..00000000 --- a/migrations/Version20241030214842.php +++ /dev/null @@ -1,30 +0,0 @@ -addSql('ALTER TABLE cw_sylius_tpay_credt_card CHANGE id id BINARY(16) NOT NULL COMMENT \'(DC2Type:uuid)\''); - } -} From 588d39e37357278f4733f9c5bc4d5785b4b631c8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=81ukasz=20Chru=C5=9Bciel?= Date: Thu, 31 Oct 2024 11:47:05 +0100 Subject: [PATCH 20/20] Bring back old validation behaviour --- assets/shop/js/card_form.js | 22 ---------------------- 1 file changed, 22 deletions(-) diff --git a/assets/shop/js/card_form.js b/assets/shop/js/card_form.js index 93a8b5e0..652c082f 100644 --- a/assets/shop/js/card_form.js +++ b/assets/shop/js/card_form.js @@ -4,7 +4,6 @@ const MAX_CARD_NUMBER_LENGTH = 16; export class CardForm { #form; - #savedCard; #cardNumber; #cardOperatorIcon; #cardsApi; @@ -16,7 +15,6 @@ export class CardForm { constructor(selector) { this.#form = document.querySelector(selector); - this.#savedCard = this.#form.querySelector('[data-tpay-saved-card]'); this.#cardNumber = this.#form.querySelector('[data-tpay-card-number]'); this.#cardOperatorIcon = this.#form.querySelector('[data-tpay-card-operator-icon]'); this.#cardsApi = this.#form.querySelector('[data-tpay-cards-api]'); @@ -69,30 +67,18 @@ export class CardForm { } isCvcValid() { - if (!this.shouldBeValidated()) { - return true; - } - const regex = new RegExp(/^\d{3}$/); return regex.test(this.getCardCvc()); } isCardNumberValid() { - if (!this.shouldBeValidated()) { - return true; - } - const regex = new RegExp(`^\\d{${MAX_CARD_NUMBER_LENGTH}}$`); return regex.test(this.getCardNumber()); } isExpirationMonthValid() { - if (!this.shouldBeValidated()) { - return true; - } - if (this.getExpirationYear() > new Date().getFullYear()) { return true; } @@ -101,17 +87,9 @@ export class CardForm { } isExpirationYearValid() { - if (!this.shouldBeValidated()) { - return true; - } - return this.getExpirationYear() >= new Date().getFullYear(); } - shouldBeValidated() { - return this.#savedCard.value === ''; - } - getCardHolderName() { return this.#form.querySelector('[data-tpay-card-holder-name]').value.trim(); }