Skip to content

Commit

Permalink
Fix support for saved card payments over API
Browse files Browse the repository at this point in the history
  • Loading branch information
lchrusciel committed Oct 30, 2024
1 parent 0846382 commit 69372c7
Show file tree
Hide file tree
Showing 15 changed files with 118 additions and 42 deletions.
2 changes: 1 addition & 1 deletion config/serialization/Pay.xml
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@
<attribute name="saveCard">
<group>commerce_weavers_sylius_tpay:shop:order:pay</group>
</attribute>
<attribute name="savedCardUid">
<attribute name="savedCardId">
<group>commerce_weavers_sylius_tpay:shop:order:pay</group>
</attribute>
<attribute name="tpayChannelId">
Expand Down
6 changes: 6 additions & 0 deletions config/services/api/command.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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')
Expand Down
10 changes: 10 additions & 0 deletions config/services/api/factory.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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')
;
Expand Down
24 changes: 20 additions & 4 deletions config/validation/Pay.xml
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,17 @@
<value>commerce_weavers_sylius_tpay:shop:order:pay</value>
</option>
</constraint>
<constraint name="CommerceWeavers\SyliusTpayPlugin\Api\Validator\Constraint\OneOfPropertiesRequiredIfGatewayConfigTypeEquals">
<option name="paymentMethodType">card</option>
<option name="properties">
<value>encodedCardData</value>
<value>savedCardId</value>
</option>
<option name="allFieldsAreBlankErrorMessage">commerce_weavers_sylius_tpay.shop.pay.card.required_fields</option>
<option name="groups">
<value>commerce_weavers_sylius_tpay:shop:order:pay</value>
</option>
</constraint>
<property name="applePayToken">
<constraint name="CommerceWeavers\SyliusTpayPlugin\Api\Validator\Constraint\NotBlankIfGatewayConfigTypeEquals">
<option name="paymentMethodType">apple_pay</option>
Expand Down Expand Up @@ -58,10 +69,15 @@
</option>
</constraint>
</property>
<property name="encodedCardData">
<constraint name="CommerceWeavers\SyliusTpayPlugin\Api\Validator\Constraint\NotBlankIfGatewayConfigTypeEquals">
<option name="paymentMethodType">card</option>
<option name="fieldRequiredErrorMessage">commerce_weavers_sylius_tpay.shop.pay.encoded_card_data.required</option>
<property name="savedCardId">
<constraint name="CommerceWeavers\SyliusTpayPlugin\Validator\Constraint\ForAuthorizedUserOnly">
<option name="groups">
<value>commerce_weavers_sylius_tpay:shop:order:pay</value>
</option>
</constraint>
</property>
<property name="saveCard">
<constraint name="CommerceWeavers\SyliusTpayPlugin\Validator\Constraint\ForAuthorizedUserOnly">
<option name="groups">
<value>commerce_weavers_sylius_tpay:shop:order:pay</value>
</option>
Expand Down
2 changes: 1 addition & 1 deletion src/Api/Command/Pay.php
Original file line number Diff line number Diff line change
Expand Up @@ -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,
) {
Expand Down
3 changes: 2 additions & 1 deletion src/Api/Command/PayBySavedCardHandler.php
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
2 changes: 1 addition & 1 deletion src/Api/Factory/NextCommand/PayByCardFactory.php
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
1 change: 0 additions & 1 deletion src/Form/EventListener/AddSavedCreditCardsListener.php
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions src/Model/PaymentDetails.php
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
4 changes: 3 additions & 1 deletion src/Payum/Action/Api/PayWithCardAction.php
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ Sylius\Component\Core\Model\Customer:
lastName: 'Doe'
email: '[email protected]'
emailCanonical: '[email protected]'

Sylius\Component\Core\Model\ShopUser:
user_john_doe:
plainPassword: '123pa\\$\\$word'
Expand Down
Original file line number Diff line number Diff line change
@@ -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'
91 changes: 60 additions & 31 deletions tests/Api/Shop/PayingForOrdersByCardTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -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');
Expand Down Expand Up @@ -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(),
);
}
Expand All @@ -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.',
]
]);
}
Expand All @@ -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' => '',
]];
}
}
2 changes: 2 additions & 0 deletions translations/validators.en.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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.'
Expand All @@ -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.'
Expand Down
2 changes: 2 additions & 0 deletions translations/validators.pl.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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.'
Expand All @@ -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.'
Expand Down

0 comments on commit 69372c7

Please sign in to comment.