From 69372c78607fd6d2984fc94a88bcb32667b7c596 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] 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 2fc2f1e4..bd39151e 100644 --- a/src/Model/PaymentDetails.php +++ b/src/Model/PaymentDetails.php @@ -200,6 +200,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.'