From cc3db266b1742497f8b9e82644eec4b03a6b7fa3 Mon Sep 17 00:00:00 2001 From: Jacob Tobiasz Date: Fri, 18 Oct 2024 10:43:41 +0200 Subject: [PATCH 01/14] Handle Apple Pay payment via UI --- src/Tpay/TpayApi.php | 1 - 1 file changed, 1 deletion(-) diff --git a/src/Tpay/TpayApi.php b/src/Tpay/TpayApi.php index 620b2cce..a9fba147 100644 --- a/src/Tpay/TpayApi.php +++ b/src/Tpay/TpayApi.php @@ -60,7 +60,6 @@ public function applePay(): ApplePayApi */ private function authorize(): void { - /** @phpstan-ignore-next-line */ if ( $this->token instanceof Token && time() <= $this->token->issued_at->getValue() + $this->token->expires_in->getValue() From 7e569eba5e035e01847fb03de7fd62c25fc4ce04 Mon Sep 17 00:00:00 2001 From: Jacob Tobiasz Date: Fri, 18 Oct 2024 12:35:29 +0200 Subject: [PATCH 02/14] Fix CI --- tests/mockoon_tpay.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/mockoon_tpay.json b/tests/mockoon_tpay.json index 9a28d4f1..91eb1bb0 100644 --- a/tests/mockoon_tpay.json +++ b/tests/mockoon_tpay.json @@ -319,4 +319,4 @@ ], "data": [], "callbacks": [] -} \ No newline at end of file +} From e7f2fd2b842f61e1a8fef29cbfc483de50581a46 Mon Sep 17 00:00:00 2001 From: Jacob Tobiasz Date: Mon, 21 Oct 2024 15:46:31 +0200 Subject: [PATCH 03/14] Make `AwareContextBuilder`s more generic and reusable --- config/services/api/context_builder.php | 10 +- .../Contract/PaymentIdAwareInterface.php | 10 ++ .../AbstractAwareContextBuilder.php | 68 +++++++++++++ ...e.php => AwareContextBuilderInterface.php} | 8 +- .../OrderTokenAwareContextBuilder.php | 52 ++-------- .../PaymentIdAwareContextBuilder.php | 25 +++++ .../OrderTokenAwareContextBuilderTest.php | 24 ++++- .../PaymentIdAwareContextBuilderTest.php | 97 +++++++++++++++++++ 8 files changed, 242 insertions(+), 52 deletions(-) create mode 100644 src/Api/Command/Contract/PaymentIdAwareInterface.php create mode 100644 src/Api/Serializer/ContextBuilder/AbstractAwareContextBuilder.php rename src/Api/Serializer/ContextBuilder/{OrderTokenAwareContextBuilderInterface.php => AwareContextBuilderInterface.php} (56%) create mode 100644 src/Api/Serializer/ContextBuilder/PaymentIdAwareContextBuilder.php create mode 100644 tests/Unit/Api/Serializer/ContextBuilder/PaymentIdAwareContextBuilderTest.php diff --git a/config/services/api/context_builder.php b/config/services/api/context_builder.php index 62e8ebe1..0a05cdb8 100644 --- a/config/services/api/context_builder.php +++ b/config/services/api/context_builder.php @@ -5,7 +5,7 @@ namespace Symfony\Component\DependencyInjection\Loader\Configurator; use CommerceWeavers\SyliusTpayPlugin\Api\Serializer\ContextBuilder\OrderTokenAwareContextBuilder; -use CommerceWeavers\SyliusTpayPlugin\Api\Serializer\ContextBuilder\OrderTokenAwareContextBuilderInterface; +use CommerceWeavers\SyliusTpayPlugin\Api\Serializer\ContextBuilder\PaymentIdAwareContextBuilder; return function(ContainerConfigurator $container): void { $services = $container->services(); @@ -15,6 +15,12 @@ ->args([ service('.inner'), ]) - ->alias(OrderTokenAwareContextBuilderInterface::class, 'commerce_weavers_sylius_tpay.api.serializer.context_builder.order_token_aware.inner') + ; + + $services->set('commerce_weavers_sylius_tpay.api.serializer.context_builder.payment_id_aware', PaymentIdAwareContextBuilder::class) + ->decorate('api_platform.serializer.context_builder') + ->args([ + service('.inner'), + ]) ; }; diff --git a/src/Api/Command/Contract/PaymentIdAwareInterface.php b/src/Api/Command/Contract/PaymentIdAwareInterface.php new file mode 100644 index 00000000..5302c364 --- /dev/null +++ b/src/Api/Command/Contract/PaymentIdAwareInterface.php @@ -0,0 +1,10 @@ +decoratedContextBuilder->createFromRequest($request, $normalization, $extractedAttributes); + + if (!$this->supports($request, $context, $extractedAttributes)) { + return $context; + } + + /** @var string $inputClass */ + $inputClass = $this->getInputClassFrom($context); + $constructorArgumentName = $this->getConstructorArgumentName($inputClass); + + $context[AbstractNormalizer::DEFAULT_CONSTRUCTOR_ARGUMENTS][$inputClass] = array_merge( + $context[AbstractNormalizer::DEFAULT_CONSTRUCTOR_ARGUMENTS][$inputClass] ?? [], + [$constructorArgumentName => $request->attributes->get($this->getAttributeKey())], + ); + + return $context; + } + + public function supports(Request $request, array $context, ?array $extractedAttributes): bool + { + $inputClass = $this->getInputClassFrom($context); + + if (null === $inputClass) { + return false; + } + + return is_a($inputClass, $this->getSupportedInterface(), true); + } + + private function getInputClassFrom(array $context): ?string + { + return $context['input']['class'] ?? null; + } + + private function getConstructorArgumentName(string $inputClass): string + { + if (!is_a($inputClass, $this->getSupportedInterface(), true)) { + throw new \InvalidArgumentException(sprintf('The class "%s" must implement "%s".', $inputClass, $this->getSupportedInterface())); + } + + return [$inputClass, $this->getPropertyNameAccessorMethodName()](); + } + + abstract public function getAttributeKey(): string; + + abstract public function getSupportedInterface(): string; + + abstract public function getPropertyNameAccessorMethodName(): string; +} diff --git a/src/Api/Serializer/ContextBuilder/OrderTokenAwareContextBuilderInterface.php b/src/Api/Serializer/ContextBuilder/AwareContextBuilderInterface.php similarity index 56% rename from src/Api/Serializer/ContextBuilder/OrderTokenAwareContextBuilderInterface.php rename to src/Api/Serializer/ContextBuilder/AwareContextBuilderInterface.php index 74a39400..ad78bbe2 100644 --- a/src/Api/Serializer/ContextBuilder/OrderTokenAwareContextBuilderInterface.php +++ b/src/Api/Serializer/ContextBuilder/AwareContextBuilderInterface.php @@ -7,7 +7,13 @@ use ApiPlatform\Serializer\SerializerContextBuilderInterface; use Symfony\Component\HttpFoundation\Request; -interface OrderTokenAwareContextBuilderInterface extends SerializerContextBuilderInterface +interface AwareContextBuilderInterface extends SerializerContextBuilderInterface { + public function getAttributeKey(): string; + + public function getSupportedInterface(): string; + + public function getPropertyNameAccessorMethodName(): string; + public function supports(Request $request, array $context, ?array $extractedAttributes): bool; } diff --git a/src/Api/Serializer/ContextBuilder/OrderTokenAwareContextBuilder.php b/src/Api/Serializer/ContextBuilder/OrderTokenAwareContextBuilder.php index f77b0793..a43cbc54 100644 --- a/src/Api/Serializer/ContextBuilder/OrderTokenAwareContextBuilder.php +++ b/src/Api/Serializer/ContextBuilder/OrderTokenAwareContextBuilder.php @@ -4,60 +4,22 @@ namespace CommerceWeavers\SyliusTpayPlugin\Api\Serializer\ContextBuilder; -use ApiPlatform\Serializer\SerializerContextBuilderInterface; use CommerceWeavers\SyliusTpayPlugin\Api\Command\Contract\OrderTokenAwareInterface; -use Symfony\Component\HttpFoundation\Request; -use Symfony\Component\Serializer\Normalizer\AbstractNormalizer; -final class OrderTokenAwareContextBuilder implements OrderTokenAwareContextBuilderInterface +final class OrderTokenAwareContextBuilder extends AbstractAwareContextBuilder { - public function __construct( - private readonly SerializerContextBuilderInterface $decoratedContextBuilder, - ) { - } - - public function createFromRequest(Request $request, bool $normalization, array $extractedAttributes = null): array - { - $context = $this->decoratedContextBuilder->createFromRequest($request, $normalization, $extractedAttributes); - - if (!$this->supports($request, $context, $extractedAttributes)) { - return $context; - } - - /** @var string $inputClass */ - $inputClass = $this->getInputClassFrom($context); - $constructorArgumentName = $this->getConstructorArgumentName($inputClass); - - $context[AbstractNormalizer::DEFAULT_CONSTRUCTOR_ARGUMENTS][$inputClass] = array_merge( - $context[AbstractNormalizer::DEFAULT_CONSTRUCTOR_ARGUMENTS][$inputClass] ?? [], - [$constructorArgumentName => $request->attributes->get('tokenValue')], - ); - - return $context; - } - - public function supports(Request $request, array $context, ?array $extractedAttributes): bool + public function getAttributeKey(): string { - $inputClass = $this->getInputClassFrom($context); - - if (null === $inputClass) { - return false; - } - - return is_a($inputClass, OrderTokenAwareInterface::class, true); + return 'tokenValue'; } - private function getInputClassFrom(array $context): ?string + public function getSupportedInterface(): string { - return $context['input']['class'] ?? null; + return OrderTokenAwareInterface::class; } - private function getConstructorArgumentName(string $inputClass): string + public function getPropertyNameAccessorMethodName(): string { - if (!is_a($inputClass, OrderTokenAwareInterface::class, true)) { - throw new \InvalidArgumentException(sprintf('The class "%s" must implement "%s".', $inputClass, OrderTokenAwareInterface::class)); - } - - return [$inputClass, 'getOrderTokenPropertyName'](); + return 'getOrderTokenPropertyName'; } } diff --git a/src/Api/Serializer/ContextBuilder/PaymentIdAwareContextBuilder.php b/src/Api/Serializer/ContextBuilder/PaymentIdAwareContextBuilder.php new file mode 100644 index 00000000..2bddf4f9 --- /dev/null +++ b/src/Api/Serializer/ContextBuilder/PaymentIdAwareContextBuilder.php @@ -0,0 +1,25 @@ +decoratedContextBuilder = $this->prophesize(SerializerContextBuilderInterface::class); } + public function test_it_returns_its_attribute_key(): void + { + $this->assertSame('tokenValue', $this->createTestSubject()->getAttributeKey()); + } + + public function test_it_returns_supported_interface(): void + { + $this->assertSame(OrderTokenAwareInterface::class, $this->createTestSubject()->getSupportedInterface()); + } + + public function test_it_returns_property_name_accessor_method_name(): void + { + $this->assertSame('getOrderTokenPropertyName', $this->createTestSubject()->getPropertyNameAccessorMethodName()); + } + public function test_it_does_not_support_a_request_without_an_input_class(): void { $isSupported = $this->createTestSubject()->supports($this->prophesize(Request::class)->reveal(), [], null); @@ -34,11 +50,11 @@ public function test_it_does_not_support_a_request_without_an_input_class(): voi public function test_it_does_not_support_a_request_with_an_input_class_that_does_not_implement_order_token_aware_interface(): void { - $context = ['input' => ['class' => Pay::class]]; + $context = ['input' => ['class' => \stdClass::class]]; $isSupported = $this->createTestSubject()->supports($this->prophesize(Request::class)->reveal(), $context, null); - $this->assertTrue($isSupported); + $this->assertFalse($isSupported); } public function test_it_returns_a_context_from_a_decorated_service_if_it_does_not_support_the_request(): void @@ -74,7 +90,7 @@ public function test_it_adds_an_order_token_value_to_default_constructor_argumen ], $result); } - private function createTestSubject(): OrderTokenAwareContextBuilderInterface + private function createTestSubject(): AwareContextBuilderInterface { return new OrderTokenAwareContextBuilder($this->decoratedContextBuilder->reveal()); } diff --git a/tests/Unit/Api/Serializer/ContextBuilder/PaymentIdAwareContextBuilderTest.php b/tests/Unit/Api/Serializer/ContextBuilder/PaymentIdAwareContextBuilderTest.php new file mode 100644 index 00000000..e332dcf4 --- /dev/null +++ b/tests/Unit/Api/Serializer/ContextBuilder/PaymentIdAwareContextBuilderTest.php @@ -0,0 +1,97 @@ +decoratedContextBuilder = $this->prophesize(SerializerContextBuilderInterface::class); + } + + public function test_it_returns_its_attribute_key(): void + { + $this->assertSame('paymentId', $this->createTestSubject()->getAttributeKey()); + } + + public function test_it_returns_supported_interface(): void + { + $this->assertSame(PaymentIdAwareInterface::class, $this->createTestSubject()->getSupportedInterface()); + } + + public function test_it_returns_property_name_accessor_method_name(): void + { + $this->assertSame('getPaymentIdPropertyName', $this->createTestSubject()->getPropertyNameAccessorMethodName()); + } + + public function test_it_does_not_support_a_request_without_an_input_class(): void + { + $isSupported = $this->createTestSubject()->supports($this->prophesize(Request::class)->reveal(), [], null); + + $this->assertFalse($isSupported); + } + + public function test_it_does_not_support_a_request_with_an_input_class_that_does_not_implement_payment_id_aware_interface(): void + { + $context = ['input' => ['class' => \stdClass::class]]; + + $isSupported = $this->createTestSubject()->supports($this->prophesize(Request::class)->reveal(), $context, null); + + $this->assertFalse($isSupported); + } + + public function test_it_returns_a_context_from_a_decorated_service_if_it_does_not_support_the_request(): void + { + $request = $this->prophesize(Request::class)->reveal(); + + $this->decoratedContextBuilder->createFromRequest($request, true, [])->willReturn(['baz' => 'qux']); + + $result = $this->createTestSubject()->createFromRequest($request, true, []); + + $this->assertSame(['baz' => 'qux'], $result); + } + + public function test_it_adds_a_payment_id_to_default_constructor_arguments_for_supported_requests(): void + { + $attributes = $this->prophesize(ParameterBag::class); + $attributes->get('paymentId')->willReturn(1); + + $request = $this->prophesize(Request::class); + $request->attributes = $attributes->reveal(); + + $this->decoratedContextBuilder->createFromRequest($request, true, [])->willReturn(['input' => ['class' => InitializeApplePaySession::class]]); + + $result = $this->createTestSubject()->createFromRequest($request->reveal(), true, []); + + $this->assertSame([ + 'input' => [ + 'class' => InitializeApplePaySession::class, + ], + 'default_constructor_arguments' => [ + InitializeApplePaySession::class => ['paymentId' => 1], + ], + ], $result); + } + + private function createTestSubject(): AwareContextBuilderInterface + { + return new PaymentIdAwareContextBuilder($this->decoratedContextBuilder->reveal()); + } +} From e970933dc9079ab90e5f210867d72e5e04ed8a2e Mon Sep 17 00:00:00 2001 From: Jacob Tobiasz Date: Mon, 21 Oct 2024 16:12:03 +0200 Subject: [PATCH 04/14] Simplify logic behind providing allowed order operations --- config/services/api/doctrine.php | 7 ++++++ .../OrderShopUserItemExtension.php | 10 +++----- .../OrderVisitorItemExtension.php | 16 +++--------- .../AllowedOrderOperationsProvider.php | 19 ++++++++++++++ ...llowedOrderOperationsProviderInterface.php | 13 ++++++++++ .../AllowedOrderOperationsProviderTest.php | 25 +++++++++++++++++++ 6 files changed, 71 insertions(+), 19 deletions(-) create mode 100644 src/Api/Doctrine/QueryItemExtension/Provider/AllowedOrderOperationsProvider.php create mode 100644 src/Api/Doctrine/QueryItemExtension/Provider/AllowedOrderOperationsProviderInterface.php create mode 100644 tests/Unit/Api/Doctrine/QueryItemExtension/Provider/AllowedOrderOperationsProviderTest.php diff --git a/config/services/api/doctrine.php b/config/services/api/doctrine.php index fc8bf074..5d4b2d7e 100644 --- a/config/services/api/doctrine.php +++ b/config/services/api/doctrine.php @@ -6,16 +6,22 @@ use CommerceWeavers\SyliusTpayPlugin\Api\Doctrine\QueryItemExtension\OrderShopUserItemExtension; use CommerceWeavers\SyliusTpayPlugin\Api\Doctrine\QueryItemExtension\OrderVisitorItemExtension; +use CommerceWeavers\SyliusTpayPlugin\Api\Doctrine\QueryItemExtension\Provider\AllowedOrderOperationsProvider; +use CommerceWeavers\SyliusTpayPlugin\Api\Doctrine\QueryItemExtension\Provider\AllowedOrderOperationsProviderInterface; use Sylius\Bundle\ApiBundle\Context\UserContextInterface; return function(ContainerConfigurator $container): void { $services = $container->services(); + $services->set('commerce_weavers_sylius_tpay.api.doctrine.query_item_extension.provider.allowed_order_operations', AllowedOrderOperationsProvider::class) + ->alias(AllowedOrderOperationsProviderInterface::class, 'commerce_weavers_sylius_tpay.api.doctrine.query_item_extension.provider.allowed_order_operations') + ; $services->set('commerce_weavers_sylius_tpay.api.doctrine.query_item_extension.order_shop_user', OrderShopUserItemExtension::class) ->decorate(\Sylius\Bundle\ApiBundle\Doctrine\QueryItemExtension\OrderShopUserItemExtension::class) ->args([ service('.inner'), service(UserContextInterface::class), + service('commerce_weavers_sylius_tpay.api.doctrine.query_item_extension.provider.allowed_order_operations'), ]) ; @@ -24,6 +30,7 @@ ->args([ service('.inner'), service(UserContextInterface::class), + service('commerce_weavers_sylius_tpay.api.doctrine.query_item_extension.provider.allowed_order_operations'), ]) ; }; diff --git a/src/Api/Doctrine/QueryItemExtension/OrderShopUserItemExtension.php b/src/Api/Doctrine/QueryItemExtension/OrderShopUserItemExtension.php index adc9383d..8cc61f95 100644 --- a/src/Api/Doctrine/QueryItemExtension/OrderShopUserItemExtension.php +++ b/src/Api/Doctrine/QueryItemExtension/OrderShopUserItemExtension.php @@ -6,6 +6,7 @@ use ApiPlatform\Core\Bridge\Doctrine\Orm\Extension\QueryItemExtensionInterface; use ApiPlatform\Core\Bridge\Doctrine\Orm\Util\QueryNameGeneratorInterface as LegacyQueryNameGeneratorInterface; +use CommerceWeavers\SyliusTpayPlugin\Api\Doctrine\QueryItemExtension\Provider\AllowedOrderOperationsProviderInterface; use Doctrine\ORM\QueryBuilder; use Sylius\Bundle\ApiBundle\Context\UserContextInterface; use Sylius\Component\Core\Model\OrderInterface; @@ -13,13 +14,10 @@ final class OrderShopUserItemExtension implements QueryItemExtensionInterface { - public const SHOP_PAY_OPERATION = 'shop_pay'; - - public const SHOP_CANCEL_LAST_PAYMENT_OPERATION = 'shop_cancel_last_payment'; - public function __construct( private readonly QueryItemExtensionInterface $decorated, private readonly UserContextInterface $userContext, + private readonly AllowedOrderOperationsProviderInterface $allowedOrderOperationsProvider, ) { } @@ -35,7 +33,7 @@ public function applyToItem( return; } - if (!in_array($operationName, $this->getAllowedOperations(), true)) { + if (!in_array($operationName, $this->allowedOrderOperationsProvider->provide(), true)) { $this->decorated->applyToItem($queryBuilder, $queryNameGenerator, $resourceClass, $identifiers, $operationName, $context); return; @@ -67,6 +65,6 @@ public function applyToItem( */ private function getAllowedOperations(): array { - return [self::SHOP_PAY_OPERATION, self::SHOP_CANCEL_LAST_PAYMENT_OPERATION]; + return [self::SHOP_PAY_OPERATION, self::SHOP_CANCEL_LAST_PAYMENT_OPERATION, self::SHOP_INITIALIZE_APPLE_PAY_SESSION_OPERATION]; } } diff --git a/src/Api/Doctrine/QueryItemExtension/OrderVisitorItemExtension.php b/src/Api/Doctrine/QueryItemExtension/OrderVisitorItemExtension.php index 22492e50..c60dfef8 100644 --- a/src/Api/Doctrine/QueryItemExtension/OrderVisitorItemExtension.php +++ b/src/Api/Doctrine/QueryItemExtension/OrderVisitorItemExtension.php @@ -6,19 +6,17 @@ use ApiPlatform\Core\Bridge\Doctrine\Orm\Extension\QueryItemExtensionInterface; use ApiPlatform\Core\Bridge\Doctrine\Orm\Util\QueryNameGeneratorInterface as LegacyQueryNameGeneratorInterface; +use CommerceWeavers\SyliusTpayPlugin\Api\Doctrine\QueryItemExtension\Provider\AllowedOrderOperationsProviderInterface; use Doctrine\ORM\QueryBuilder; use Sylius\Bundle\ApiBundle\Context\UserContextInterface; use Sylius\Component\Core\Model\OrderInterface; final class OrderVisitorItemExtension implements QueryItemExtensionInterface { - public const SHOP_PAY_OPERATION = 'shop_pay'; - - public const SHOP_CANCEL_LAST_PAYMENT_OPERATION = 'shop_cancel_last_payment'; - public function __construct( private readonly QueryItemExtensionInterface $decorated, private readonly UserContextInterface $userContext, + private readonly AllowedOrderOperationsProviderInterface $allowedOrderOperationsProvider, ) { } @@ -34,7 +32,7 @@ public function applyToItem( return; } - if (!in_array($operationName, $this->getAllowedOperations(), true)) { + if (!in_array($operationName, $this->allowedOrderOperationsProvider->provide(), true)) { $this->decorated->applyToItem($queryBuilder, $queryNameGenerator, $resourceClass, $identifiers, $operationName, $context); return; @@ -60,12 +58,4 @@ public function applyToItem( ))->setParameter('createdByGuest', true) ; } - - /** - * @return array - */ - private function getAllowedOperations(): array - { - return [self::SHOP_PAY_OPERATION, self::SHOP_CANCEL_LAST_PAYMENT_OPERATION]; - } } diff --git a/src/Api/Doctrine/QueryItemExtension/Provider/AllowedOrderOperationsProvider.php b/src/Api/Doctrine/QueryItemExtension/Provider/AllowedOrderOperationsProvider.php new file mode 100644 index 00000000..d02fc5be --- /dev/null +++ b/src/Api/Doctrine/QueryItemExtension/Provider/AllowedOrderOperationsProvider.php @@ -0,0 +1,19 @@ + + */ + public function provide(): array; +} diff --git a/tests/Unit/Api/Doctrine/QueryItemExtension/Provider/AllowedOrderOperationsProviderTest.php b/tests/Unit/Api/Doctrine/QueryItemExtension/Provider/AllowedOrderOperationsProviderTest.php new file mode 100644 index 00000000..b956b90b --- /dev/null +++ b/tests/Unit/Api/Doctrine/QueryItemExtension/Provider/AllowedOrderOperationsProviderTest.php @@ -0,0 +1,25 @@ +assertSame( + [ + AllowedOrderOperationsProvider::SHOP_PAY_OPERATION, + AllowedOrderOperationsProvider::SHOP_CANCEL_LAST_PAYMENT_OPERATION, + AllowedOrderOperationsProvider::SHOP_INITIALIZE_APPLE_PAY_SESSION_OPERATION, + ], + $provider->provide() + ); + } +} From 224bc742bfed9b1638706e6f0bef4e1a661d627c Mon Sep 17 00:00:00 2001 From: Jacob Tobiasz Date: Mon, 21 Oct 2024 16:43:10 +0200 Subject: [PATCH 05/14] Create a command responsible for initializing an Apple Pay session --- config/api_resources/order.yaml | 27 +++++++++++++++++ .../InitializeApplePaySession.xml | 29 ++++++++++++++++++ src/Api/Command/InitializeApplePaySession.php | 30 +++++++++++++++++++ 3 files changed, 86 insertions(+) create mode 100644 config/serialization/InitializeApplePaySession.xml create mode 100644 src/Api/Command/InitializeApplePaySession.php diff --git a/config/api_resources/order.yaml b/config/api_resources/order.yaml index b94a245e..05479303 100644 --- a/config/api_resources/order.yaml +++ b/config/api_resources/order.yaml @@ -16,6 +16,33 @@ - 'commerce_weavers_sylius_tpay:shop:order:pay' openapi_context: summary: 'Pay for the order' + shop_initialize_apple_pay_session: + method: 'POST' + path: '/shop/orders/{tokenValue}/payments/{paymentId}/apple-pay-session' + messenger: 'input' + input: 'CommerceWeavers\SyliusTpayPlugin\Api\Command\InitializeApplePaySession' + status: 200 + denormalization_context: + groups: + - 'commerce_weavers_sylius_tpay:shop:order:payment:initialize_apple_pay_session' + normalization_context: + groups: + - 'commerce_weavers_sylius_tpay:shop:order:payment:initialize_apple_pay_session_result' + validation_groups: + - 'commerce_weavers_sylius_tpay:shop:order:payment:initialize_apple_pay_session' + openapi_context: + summary: 'Initialize Apple Pay payment session' + parameters: + - name: tokenValue + in: path + required: true + schema: + type: string + - name: paymentId + in: path + required: true + schema: + type: integer shop_cancel_last_payment: method: 'PATCH' path: '/shop/orders/{tokenValue}/cancel-last-payment' diff --git a/config/serialization/InitializeApplePaySession.xml b/config/serialization/InitializeApplePaySession.xml new file mode 100644 index 00000000..5c0a4610 --- /dev/null +++ b/config/serialization/InitializeApplePaySession.xml @@ -0,0 +1,29 @@ + + + + + + + + commerce_weavers_sylius_tpay:shop:order:payment:initialize_apple_pay_session + + + commerce_weavers_sylius_tpay:shop:order:payment:initialize_apple_pay_session + + + commerce_weavers_sylius_tpay:shop:order:payment:initialize_apple_pay_session + + + diff --git a/src/Api/Command/InitializeApplePaySession.php b/src/Api/Command/InitializeApplePaySession.php new file mode 100644 index 00000000..19de3b89 --- /dev/null +++ b/src/Api/Command/InitializeApplePaySession.php @@ -0,0 +1,30 @@ + Date: Mon, 21 Oct 2024 18:01:10 +0200 Subject: [PATCH 06/14] Create a result object for initializing an Apple Pay session --- .../InitializeApplePaySessionResult.xml | 26 +++++++++++++++++++ config/services/api/command.php | 10 ++++++- .../InitializeApplePaySessionResult.php | 14 ++++++++++ 3 files changed, 49 insertions(+), 1 deletion(-) create mode 100644 config/serialization/InitializeApplePaySessionResult.xml create mode 100644 src/Api/Command/InitializeApplePaySessionResult.php diff --git a/config/serialization/InitializeApplePaySessionResult.xml b/config/serialization/InitializeApplePaySessionResult.xml new file mode 100644 index 00000000..bef05944 --- /dev/null +++ b/config/serialization/InitializeApplePaySessionResult.xml @@ -0,0 +1,26 @@ + + + + + + + + commerce_weavers_sylius_tpay:shop:order:payment:initialize_apple_pay_session_result + + + commerce_weavers_sylius_tpay:shop:order:payment:initialize_apple_pay_session_result + + + diff --git a/config/services/api/command.php b/config/services/api/command.php index 8977aaae..38122c91 100644 --- a/config/services/api/command.php +++ b/config/services/api/command.php @@ -5,11 +5,12 @@ namespace Symfony\Component\DependencyInjection\Loader\Configurator; use CommerceWeavers\SyliusTpayPlugin\Api\Command\AbstractPayByHandler; +use CommerceWeavers\SyliusTpayPlugin\Api\Command\InitializeApplePaySessionHandler; use CommerceWeavers\SyliusTpayPlugin\Api\Command\PayByApplePayHandler; use CommerceWeavers\SyliusTpayPlugin\Api\Command\PayByBlikHandler; use CommerceWeavers\SyliusTpayPlugin\Api\Command\PayByCardHandler; -use CommerceWeavers\SyliusTpayPlugin\Api\Command\PayByLinkHandler; use CommerceWeavers\SyliusTpayPlugin\Api\Command\PayByGooglePayHandler; +use CommerceWeavers\SyliusTpayPlugin\Api\Command\PayByLinkHandler; use CommerceWeavers\SyliusTpayPlugin\Api\Command\PayByRedirectHandler; use CommerceWeavers\SyliusTpayPlugin\Api\Command\PayByVisaMobileHandler; use CommerceWeavers\SyliusTpayPlugin\Api\Command\PayHandler; @@ -45,6 +46,13 @@ ]) ; + $services->set('commerce_weavers_sylius_tpay.api.command.initialize_apple_pay_session_handler', InitializeApplePaySessionHandler::class) + ->args([ + service('commerce_weavers_sylius_tpay.gateway'), + ]) + ->tag('messenger.message_handler') + ; + $services->set('commerce_weavers_sylius_tpay.api.command.pay_by_apple_pay_handler', PayByApplePayHandler::class) ->parent('commerce_weavers_sylius_tpay.api.command.abstract_pay_by_handler') ->tag('messenger.message_handler') diff --git a/src/Api/Command/InitializeApplePaySessionResult.php b/src/Api/Command/InitializeApplePaySessionResult.php new file mode 100644 index 00000000..4eb142ff --- /dev/null +++ b/src/Api/Command/InitializeApplePaySessionResult.php @@ -0,0 +1,14 @@ + Date: Mon, 21 Oct 2024 18:56:12 +0200 Subject: [PATCH 07/14] Allow initializing Apple Pay session via API --- config/api_resources/order.yaml | 4 +- config/config/api_platform.php | 4 +- .../InitializeApplePaySessionResult.xml | 2 +- config/services.php | 9 +++ .../Exception/OrderCannotBeFoundException.php | 9 +++ src/Api/Command/InitializeApplePaySession.php | 9 +-- .../InitializeApplePaySessionHandler.php | 52 +++++++++++++ src/Tpay/TpayApi.php | 4 +- ...est_initializing_an_apple_pay_session.json | 12 +++ .../Shop/PayingForOrdersByApplePayTest.php | 26 +++++++ .../InitializeApplePaySessionHandlerTest.php | 78 +++++++++++++++++++ tests/mockoon_tpay.json | 33 ++++++++ 12 files changed, 228 insertions(+), 14 deletions(-) create mode 100644 src/Api/Command/Exception/OrderCannotBeFoundException.php create mode 100644 src/Api/Command/InitializeApplePaySessionHandler.php create mode 100644 tests/Api/Responses/shop/paying_for_orders_by_apple_pay/test_initializing_an_apple_pay_session.json create mode 100644 tests/Unit/Api/Command/InitializeApplePaySessionHandlerTest.php diff --git a/config/api_resources/order.yaml b/config/api_resources/order.yaml index 05479303..27567565 100644 --- a/config/api_resources/order.yaml +++ b/config/api_resources/order.yaml @@ -18,10 +18,10 @@ summary: 'Pay for the order' shop_initialize_apple_pay_session: method: 'POST' - path: '/shop/orders/{tokenValue}/payments/{paymentId}/apple-pay-session' + path: '/shop/orders/{tokenValue}/apple-pay-session' messenger: 'input' input: 'CommerceWeavers\SyliusTpayPlugin\Api\Command\InitializeApplePaySession' - status: 200 + status: 201 denormalization_context: groups: - 'commerce_weavers_sylius_tpay:shop:order:payment:initialize_apple_pay_session' diff --git a/config/config/api_platform.php b/config/config/api_platform.php index 6055dcd5..76a530f8 100644 --- a/config/config/api_platform.php +++ b/config/config/api_platform.php @@ -4,14 +4,16 @@ namespace Symfony\Component\DependencyInjection\Loader\Configurator; +use CommerceWeavers\SyliusTpayPlugin\Api\Command\Exception\OrderCannotBeFoundException; use CommerceWeavers\SyliusTpayPlugin\Api\Factory\Exception\UnresolvableNextCommandException; use CommerceWeavers\SyliusTpayPlugin\Payment\Exception\PaymentCannotBeCancelledException; return function(ContainerConfigurator $configurator): void { $configurator->extension('api_platform', [ 'exception_to_status' => [ - UnresolvableNextCommandException::class => 400, + OrderCannotBeFoundException::class => 404, PaymentCannotBeCancelledException::class => 400, + UnresolvableNextCommandException::class => 400, ], 'mapping' => [ 'paths' => [ diff --git a/config/serialization/InitializeApplePaySessionResult.xml b/config/serialization/InitializeApplePaySessionResult.xml index bef05944..293efe2d 100644 --- a/config/serialization/InitializeApplePaySessionResult.xml +++ b/config/serialization/InitializeApplePaySessionResult.xml @@ -16,7 +16,7 @@ xsi:schemaLocation="http://symfony.com/schema/dic/serializer-mapping https://symfony.com/schema/dic/serializer-mapping/serializer-mapping-1.0.xsd" > - + commerce_weavers_sylius_tpay:shop:order:payment:initialize_apple_pay_session_result diff --git a/config/services.php b/config/services.php index 82e5364a..b84b43b5 100644 --- a/config/services.php +++ b/config/services.php @@ -4,6 +4,15 @@ namespace Symfony\Component\DependencyInjection\Loader\Configurator; +use Payum\Core\Gateway; + return function(ContainerConfigurator $container): void { $container->import('services/**/*.php'); + + $services = $container->services(); + + $services->set('commerce_weavers_sylius_tpay.gateway', Gateway::class) + ->factory([service('payum'), 'getGateway']) + ->args(['tpay']) + ; }; diff --git a/src/Api/Command/Exception/OrderCannotBeFoundException.php b/src/Api/Command/Exception/OrderCannotBeFoundException.php new file mode 100644 index 00000000..eb9be51e --- /dev/null +++ b/src/Api/Command/Exception/OrderCannotBeFoundException.php @@ -0,0 +1,9 @@ +verifyOrderExist($command->orderToken); + + $this->gateway->execute( + new InitializeApplePayPayment( + new ArrayObject([ + 'domainName' => $command->domainName, + 'displayName' => $command->displayName, + 'validationUrl' => $command->validationUrl, + ]), + $output = new ArrayObject(), + ), + ); + + return new InitializeApplePaySessionResult( + $output['result'], + $output['session'], + ); + } + + private function verifyOrderExist(string $orderToken): void + { + $order = $this->orderRepository->findOneByTokenValue($orderToken); + + if (null === $order) { + throw new OrderCannotBeFoundException(sprintf('Order with token "%s" cannot be found.', $orderToken)); + } + } +} diff --git a/src/Tpay/TpayApi.php b/src/Tpay/TpayApi.php index a9fba147..73474d90 100644 --- a/src/Tpay/TpayApi.php +++ b/src/Tpay/TpayApi.php @@ -24,7 +24,7 @@ public function __construct( private readonly string $clientSecret, private readonly bool $productionMode = false, private readonly string $scope = 'read', - ?string $apiUrlOverride = null, + private readonly ?string $apiUrlOverride = null, private readonly ?string $clientName = null, private readonly ?string $notificationSecretCode = null, ) { @@ -45,7 +45,7 @@ public function applePay(): ApplePayApi Assert::notNull($this->token); $this->applePayApi = (new ApplePayApi($this->token, $this->productionMode)) - ->overrideApiUrl($this->apiUrl); + ->overrideApiUrl($this->apiUrlOverride); if ($this->clientName) { $this->applePayApi->setClientName($this->clientName); diff --git a/tests/Api/Responses/shop/paying_for_orders_by_apple_pay/test_initializing_an_apple_pay_session.json b/tests/Api/Responses/shop/paying_for_orders_by_apple_pay/test_initializing_an_apple_pay_session.json new file mode 100644 index 00000000..f5f9876f --- /dev/null +++ b/tests/Api/Responses/shop/paying_for_orders_by_apple_pay/test_initializing_an_apple_pay_session.json @@ -0,0 +1,12 @@ +{ + "@context": { + "@vocab": "http://localhost/api/v2/docs.jsonld#", + "hydra": "http://www.w3.org/ns/hydra/core#", + "result": "InitializeApplePaySessionResult/result", + "session": "InitializeApplePaySessionResult/session" + }, + "@type": "InitializeApplePaySessionResult", + "@id": "/api/v2/.well-known/genid/@string@", + "result": "success", + "session": "apple-pay-token" +} diff --git a/tests/Api/Shop/PayingForOrdersByApplePayTest.php b/tests/Api/Shop/PayingForOrdersByApplePayTest.php index 53a9f24f..aa2b58df 100644 --- a/tests/Api/Shop/PayingForOrdersByApplePayTest.php +++ b/tests/Api/Shop/PayingForOrdersByApplePayTest.php @@ -23,6 +23,32 @@ protected function setUp(): void $this->loadFixturesFromFile('shop/paying_for_orders_by_apple_pay.yml'); } + public function test_it_initializes_an_apple_pay_session(): void + { + $order = $this->doPlaceOrder('t0k3n', paymentMethodCode: 'tpay_apple_pay'); + + $this->client->request( + Request::METHOD_POST, + sprintf('/api/v2/shop/orders/%s/apple-pay-session', $order->getTokenValue()), + server: self::CONTENT_TYPE_HEADER, + content: json_encode([ + 'successUrl' => 'https://example.com/success', + 'failureUrl' => 'https://example.com/failure', + 'domainName' => 'cw.nonexisting', + 'displayName' => 'Commerce Weavers', + 'validationUrl' => 'https://cw.nonexisting/validation', + ]), + ); + + $response = $this->client->getResponse(); + + $this->assertResponse( + $response, + 'shop/paying_for_orders_by_apple_pay/test_initializing_an_apple_pay_session', + Response::HTTP_CREATED, + ); + } + public function test_paying_with_a_valid_apple_pay_token_for_an_order(): void { $order = $this->doPlaceOrder('t0k3n', paymentMethodCode: 'tpay_apple_pay'); diff --git a/tests/Unit/Api/Command/InitializeApplePaySessionHandlerTest.php b/tests/Unit/Api/Command/InitializeApplePaySessionHandlerTest.php new file mode 100644 index 00000000..ea5ca7d6 --- /dev/null +++ b/tests/Unit/Api/Command/InitializeApplePaySessionHandlerTest.php @@ -0,0 +1,78 @@ +orderRepository = $this->prophesize(OrderRepositoryInterface::class); + $this->gateway = $this->prophesize(GatewayInterface::class); + } + + public function test_it_throws_an_exception_if_an_order_with_the_given_token_does_not_exist(): void + { + $this->expectException(OrderCannotBeFoundException::class); + $this->expectExceptionMessage('Order with token "t0k3n" cannot be found.'); + + $this->orderRepository->findOneByTokenValue('t0k3n')->willReturn(null); + + $this->createTestSubject()($this->createCommand()); + } + + public function test_it_initializes_an_apple_pay_session(): void + { + $order = $this->prophesize(OrderInterface::class); + + $this->orderRepository->findOneByTokenValue('t0k3n')->willReturn($order); + + $this->gateway->execute(Argument::that(function (InitializeApplePayPayment $request): bool { + $request->getOutput()->replace([ + 'result' => 'result', + 'session' => 'session', + ]); + + return true; + }))->shouldBeCalled(); + + $result = $this->createTestSubject()($this->createCommand()); + + $this->assertSame('result', $result->result); + $this->assertSame('session', $result->session); + } + + private function createCommand(): InitializeApplePaySession + { + return new InitializeApplePaySession( + orderToken: 't0k3n', + domainName: 'cw.nonexisting', + displayName: 'Commerce Weavers', + validationUrl: 'https://cw.nonexisting/validation', + ); + } + + private function createTestSubject(): InitializeApplePaySessionHandler + { + return new InitializeApplePaySessionHandler($this->orderRepository->reveal(), $this->gateway->reveal()); + } +} diff --git a/tests/mockoon_tpay.json b/tests/mockoon_tpay.json index 91eb1bb0..53d807d1 100644 --- a/tests/mockoon_tpay.json +++ b/tests/mockoon_tpay.json @@ -266,6 +266,35 @@ } ], "responseMode": null + }, + { + "uuid": "7156e806-bfaf-4a61-8635-144c74efc290", + "type": "http", + "documentation": "", + "method": "post", + "endpoint": "wallet/applepay/init", + "responses": [ + { + "uuid": "b5e188ce-1522-4f14-aa59-0e28ebb07c67", + "body": "{\n \"result\": \"success\",\n \"requestId\": \"85136c79cbf9fe36bb9\",\n \"session\": \"apple-pay-token\"\n}", + "latency": 0, + "statusCode": 200, + "label": "", + "headers": [], + "bodyType": "INLINE", + "filePath": "", + "databucketID": "", + "sendFileAsBody": false, + "rules": [], + "rulesOperator": "OR", + "disableTemplating": false, + "fallbackTo404": false, + "default": true, + "crudKey": "id", + "callbacks": [] + } + ], + "responseMode": null } ], "rootChildren": [ @@ -284,6 +313,10 @@ { "type": "route", "uuid": "e4a22d76-46eb-41c1-b11b-4691771080df" + }, + { + "type": "route", + "uuid": "7156e806-bfaf-4a61-8635-144c74efc290" } ], "proxyMode": false, From 3c5dca41b874789e38dc6b4e0819c106dc9e5de1 Mon Sep 17 00:00:00 2001 From: Jacob Tobiasz Date: Mon, 21 Oct 2024 19:07:15 +0200 Subject: [PATCH 08/14] Fix CI --- config/services/api/command.php | 1 + src/Api/Command/InitializeApplePaySession.php | 2 +- src/Api/Command/InitializeApplePaySessionHandler.php | 6 +++++- src/Api/Command/InitializeApplePaySessionResult.php | 2 +- .../QueryItemExtension/OrderShopUserItemExtension.php | 8 -------- .../ContextBuilder/AbstractAwareContextBuilder.php | 2 +- src/Tpay/TpayApi.php | 7 +++++-- tests/mockoon_tpay.json | 2 +- 8 files changed, 15 insertions(+), 15 deletions(-) diff --git a/config/services/api/command.php b/config/services/api/command.php index 38122c91..d685eee2 100644 --- a/config/services/api/command.php +++ b/config/services/api/command.php @@ -48,6 +48,7 @@ $services->set('commerce_weavers_sylius_tpay.api.command.initialize_apple_pay_session_handler', InitializeApplePaySessionHandler::class) ->args([ + service('sylius.repository.order'), service('commerce_weavers_sylius_tpay.gateway'), ]) ->tag('messenger.message_handler') diff --git a/src/Api/Command/InitializeApplePaySession.php b/src/Api/Command/InitializeApplePaySession.php index af3fa38b..132450b7 100644 --- a/src/Api/Command/InitializeApplePaySession.php +++ b/src/Api/Command/InitializeApplePaySession.php @@ -8,7 +8,7 @@ final class InitializeApplePaySession implements OrderTokenAwareInterface { - public function __construct ( + public function __construct( public readonly string $orderToken, public readonly string $domainName, public readonly string $displayName, diff --git a/src/Api/Command/InitializeApplePaySessionHandler.php b/src/Api/Command/InitializeApplePaySessionHandler.php index 5004bdd5..68110bf6 100644 --- a/src/Api/Command/InitializeApplePaySessionHandler.php +++ b/src/Api/Command/InitializeApplePaySessionHandler.php @@ -10,11 +10,12 @@ use Payum\Core\GatewayInterface; use Sylius\Component\Core\Repository\OrderRepositoryInterface; use Symfony\Component\Messenger\Attribute\AsMessageHandler; +use Webmozart\Assert\Assert; #[AsMessageHandler] final class InitializeApplePaySessionHandler { - public function __construct ( + public function __construct( private readonly OrderRepositoryInterface $orderRepository, private readonly GatewayInterface $gateway, ) { @@ -35,6 +36,9 @@ public function __invoke(InitializeApplePaySession $command): InitializeApplePay ), ); + Assert::string($output['result']); + Assert::string($output['session']); + return new InitializeApplePaySessionResult( $output['result'], $output['session'], diff --git a/src/Api/Command/InitializeApplePaySessionResult.php b/src/Api/Command/InitializeApplePaySessionResult.php index 4eb142ff..288b73c8 100644 --- a/src/Api/Command/InitializeApplePaySessionResult.php +++ b/src/Api/Command/InitializeApplePaySessionResult.php @@ -6,7 +6,7 @@ final class InitializeApplePaySessionResult { - public function __construct ( + public function __construct( public readonly string $result, public readonly string $session, ) { diff --git a/src/Api/Doctrine/QueryItemExtension/OrderShopUserItemExtension.php b/src/Api/Doctrine/QueryItemExtension/OrderShopUserItemExtension.php index 8cc61f95..8a1f799d 100644 --- a/src/Api/Doctrine/QueryItemExtension/OrderShopUserItemExtension.php +++ b/src/Api/Doctrine/QueryItemExtension/OrderShopUserItemExtension.php @@ -59,12 +59,4 @@ public function applyToItem( ->setParameter($customerParameterName, $customer->getId()) ; } - - /** - * @return array - */ - private function getAllowedOperations(): array - { - return [self::SHOP_PAY_OPERATION, self::SHOP_CANCEL_LAST_PAYMENT_OPERATION, self::SHOP_INITIALIZE_APPLE_PAY_SESSION_OPERATION]; - } } diff --git a/src/Api/Serializer/ContextBuilder/AbstractAwareContextBuilder.php b/src/Api/Serializer/ContextBuilder/AbstractAwareContextBuilder.php index 77f64483..e7f34b80 100644 --- a/src/Api/Serializer/ContextBuilder/AbstractAwareContextBuilder.php +++ b/src/Api/Serializer/ContextBuilder/AbstractAwareContextBuilder.php @@ -10,7 +10,7 @@ abstract class AbstractAwareContextBuilder implements AwareContextBuilderInterface { - public function __construct ( + public function __construct( private readonly SerializerContextBuilderInterface $decoratedContextBuilder, ) { } diff --git a/src/Tpay/TpayApi.php b/src/Tpay/TpayApi.php index 73474d90..5bbdb456 100644 --- a/src/Tpay/TpayApi.php +++ b/src/Tpay/TpayApi.php @@ -44,8 +44,11 @@ public function applePay(): ApplePayApi if (null === $this->applePayApi) { Assert::notNull($this->token); - $this->applePayApi = (new ApplePayApi($this->token, $this->productionMode)) - ->overrideApiUrl($this->apiUrlOverride); + $this->applePayApi = (new ApplePayApi($this->token, $this->productionMode)); + + if ($this->apiUrlOverride) { + $this->applePayApi->overrideApiUrl($this->apiUrlOverride); + } if ($this->clientName) { $this->applePayApi->setClientName($this->clientName); diff --git a/tests/mockoon_tpay.json b/tests/mockoon_tpay.json index 53d807d1..379f7bff 100644 --- a/tests/mockoon_tpay.json +++ b/tests/mockoon_tpay.json @@ -352,4 +352,4 @@ ], "data": [], "callbacks": [] -} +} \ No newline at end of file From 5d7cc65e187274cb925b7bed0a948d34b1a64103 Mon Sep 17 00:00:00 2001 From: Jacob Tobiasz Date: Tue, 22 Oct 2024 07:29:09 +0200 Subject: [PATCH 09/14] Remove the `PaymentIdAwareContextBuilder` --- config/services/api/context_builder.php | 7 -- .../PaymentIdAwareContextBuilder.php | 25 ----- .../PaymentIdAwareContextBuilderTest.php | 97 ------------------- 3 files changed, 129 deletions(-) delete mode 100644 src/Api/Serializer/ContextBuilder/PaymentIdAwareContextBuilder.php delete mode 100644 tests/Unit/Api/Serializer/ContextBuilder/PaymentIdAwareContextBuilderTest.php diff --git a/config/services/api/context_builder.php b/config/services/api/context_builder.php index 0a05cdb8..70c1fba5 100644 --- a/config/services/api/context_builder.php +++ b/config/services/api/context_builder.php @@ -16,11 +16,4 @@ service('.inner'), ]) ; - - $services->set('commerce_weavers_sylius_tpay.api.serializer.context_builder.payment_id_aware', PaymentIdAwareContextBuilder::class) - ->decorate('api_platform.serializer.context_builder') - ->args([ - service('.inner'), - ]) - ; }; diff --git a/src/Api/Serializer/ContextBuilder/PaymentIdAwareContextBuilder.php b/src/Api/Serializer/ContextBuilder/PaymentIdAwareContextBuilder.php deleted file mode 100644 index 2bddf4f9..00000000 --- a/src/Api/Serializer/ContextBuilder/PaymentIdAwareContextBuilder.php +++ /dev/null @@ -1,25 +0,0 @@ -decoratedContextBuilder = $this->prophesize(SerializerContextBuilderInterface::class); - } - - public function test_it_returns_its_attribute_key(): void - { - $this->assertSame('paymentId', $this->createTestSubject()->getAttributeKey()); - } - - public function test_it_returns_supported_interface(): void - { - $this->assertSame(PaymentIdAwareInterface::class, $this->createTestSubject()->getSupportedInterface()); - } - - public function test_it_returns_property_name_accessor_method_name(): void - { - $this->assertSame('getPaymentIdPropertyName', $this->createTestSubject()->getPropertyNameAccessorMethodName()); - } - - public function test_it_does_not_support_a_request_without_an_input_class(): void - { - $isSupported = $this->createTestSubject()->supports($this->prophesize(Request::class)->reveal(), [], null); - - $this->assertFalse($isSupported); - } - - public function test_it_does_not_support_a_request_with_an_input_class_that_does_not_implement_payment_id_aware_interface(): void - { - $context = ['input' => ['class' => \stdClass::class]]; - - $isSupported = $this->createTestSubject()->supports($this->prophesize(Request::class)->reveal(), $context, null); - - $this->assertFalse($isSupported); - } - - public function test_it_returns_a_context_from_a_decorated_service_if_it_does_not_support_the_request(): void - { - $request = $this->prophesize(Request::class)->reveal(); - - $this->decoratedContextBuilder->createFromRequest($request, true, [])->willReturn(['baz' => 'qux']); - - $result = $this->createTestSubject()->createFromRequest($request, true, []); - - $this->assertSame(['baz' => 'qux'], $result); - } - - public function test_it_adds_a_payment_id_to_default_constructor_arguments_for_supported_requests(): void - { - $attributes = $this->prophesize(ParameterBag::class); - $attributes->get('paymentId')->willReturn(1); - - $request = $this->prophesize(Request::class); - $request->attributes = $attributes->reveal(); - - $this->decoratedContextBuilder->createFromRequest($request, true, [])->willReturn(['input' => ['class' => InitializeApplePaySession::class]]); - - $result = $this->createTestSubject()->createFromRequest($request->reveal(), true, []); - - $this->assertSame([ - 'input' => [ - 'class' => InitializeApplePaySession::class, - ], - 'default_constructor_arguments' => [ - InitializeApplePaySession::class => ['paymentId' => 1], - ], - ], $result); - } - - private function createTestSubject(): AwareContextBuilderInterface - { - return new PaymentIdAwareContextBuilder($this->decoratedContextBuilder->reveal()); - } -} From 4431579a7fa49d03b3fe84fa8d0987802691169a Mon Sep 17 00:00:00 2001 From: Jacob Tobiasz Date: Tue, 22 Oct 2024 09:20:28 +0200 Subject: [PATCH 10/14] Remove accidental copyright notes --- config/serialization/InitializeApplePaySession.xml | 11 ----------- .../serialization/InitializeApplePaySessionResult.xml | 11 ----------- config/serialization/Pay.xml | 11 ----------- config/serialization/PayResult.xml | 11 ----------- 4 files changed, 44 deletions(-) diff --git a/config/serialization/InitializeApplePaySession.xml b/config/serialization/InitializeApplePaySession.xml index 5c0a4610..5224f68f 100644 --- a/config/serialization/InitializeApplePaySession.xml +++ b/config/serialization/InitializeApplePaySession.xml @@ -1,16 +1,5 @@ - - - - - - - - Date: Tue, 22 Oct 2024 09:20:39 +0200 Subject: [PATCH 11/14] Remove a left-over `use` statement --- config/services/api/context_builder.php | 1 - 1 file changed, 1 deletion(-) diff --git a/config/services/api/context_builder.php b/config/services/api/context_builder.php index 70c1fba5..1ebee8f0 100644 --- a/config/services/api/context_builder.php +++ b/config/services/api/context_builder.php @@ -5,7 +5,6 @@ namespace Symfony\Component\DependencyInjection\Loader\Configurator; use CommerceWeavers\SyliusTpayPlugin\Api\Serializer\ContextBuilder\OrderTokenAwareContextBuilder; -use CommerceWeavers\SyliusTpayPlugin\Api\Serializer\ContextBuilder\PaymentIdAwareContextBuilder; return function(ContainerConfigurator $container): void { $services = $container->services(); From 9476efb43a6b908c7c6674109c4c82254c788944 Mon Sep 17 00:00:00 2001 From: Jacob Tobiasz Date: Tue, 22 Oct 2024 10:10:53 +0200 Subject: [PATCH 12/14] Introduce the `InitializeApplePayPayment` factory --- config/services/api/command.php | 1 + config/services/payum/factory.php | 6 ++++ .../InitializeApplePaySessionHandler.php | 5 +-- .../InitializeApplePayPaymentFactory.php | 16 +++++++++ ...tializeApplePayPaymentFactoryInterface.php | 13 ++++++++ .../InitializeApplePayPaymentFactoryTest.php | 33 +++++++++++++++++++ 6 files changed, 72 insertions(+), 2 deletions(-) create mode 100644 src/Payum/Factory/InitializeApplePayPaymentFactory.php create mode 100644 src/Payum/Factory/InitializeApplePayPaymentFactoryInterface.php create mode 100644 tests/Unit/Payum/Factory/InitializeApplePayPaymentFactoryTest.php diff --git a/config/services/api/command.php b/config/services/api/command.php index d685eee2..5cac5b22 100644 --- a/config/services/api/command.php +++ b/config/services/api/command.php @@ -50,6 +50,7 @@ ->args([ service('sylius.repository.order'), service('commerce_weavers_sylius_tpay.gateway'), + service('commerce_weavers_sylius_tpay.payum.factory.initialize_apple_pay_payment'), ]) ->tag('messenger.message_handler') ; diff --git a/config/services/payum/factory.php b/config/services/payum/factory.php index 3670d449..0466c94e 100644 --- a/config/services/payum/factory.php +++ b/config/services/payum/factory.php @@ -6,6 +6,8 @@ use CommerceWeavers\SyliusTpayPlugin\Payum\Factory\CreateTransactionFactory; use CommerceWeavers\SyliusTpayPlugin\Payum\Factory\CreateTransactionFactoryInterface; +use CommerceWeavers\SyliusTpayPlugin\Payum\Factory\InitializeApplePayPaymentFactory; +use CommerceWeavers\SyliusTpayPlugin\Payum\Factory\InitializeApplePayPaymentFactoryInterface; use CommerceWeavers\SyliusTpayPlugin\Payum\Factory\NotifyDataFactory; use CommerceWeavers\SyliusTpayPlugin\Payum\Factory\NotifyDataFactoryInterface; use CommerceWeavers\SyliusTpayPlugin\Payum\Factory\NotifyFactory; @@ -33,6 +35,10 @@ ->alias(CreateTransactionFactoryInterface::class, 'commerce_weavers_sylius_tpay.payum.factory.create_transaction') ; + $services->set('commerce_weavers_sylius_tpay.payum.factory.initialize_apple_pay_payment', InitializeApplePayPaymentFactory::class) + ->alias(InitializeApplePayPaymentFactoryInterface::class, 'commerce_weavers_sylius_tpay.payum.factory.initialize_apple_pay_payment') + ; + $services->set('commerce_weavers_sylius_tpay.payum.factory.notify_data', NotifyDataFactory::class) ->alias(NotifyDataFactoryInterface::class, 'commerce_weavers_sylius_tpay.payum.factory.notify_data') ; diff --git a/src/Api/Command/InitializeApplePaySessionHandler.php b/src/Api/Command/InitializeApplePaySessionHandler.php index 68110bf6..707e9725 100644 --- a/src/Api/Command/InitializeApplePaySessionHandler.php +++ b/src/Api/Command/InitializeApplePaySessionHandler.php @@ -5,7 +5,7 @@ namespace CommerceWeavers\SyliusTpayPlugin\Api\Command; use CommerceWeavers\SyliusTpayPlugin\Api\Command\Exception\OrderCannotBeFoundException; -use CommerceWeavers\SyliusTpayPlugin\Payum\Request\Api\InitializeApplePayPayment; +use CommerceWeavers\SyliusTpayPlugin\Payum\Factory\InitializeApplePayPaymentFactoryInterface; use Payum\Core\Bridge\Spl\ArrayObject; use Payum\Core\GatewayInterface; use Sylius\Component\Core\Repository\OrderRepositoryInterface; @@ -18,6 +18,7 @@ final class InitializeApplePaySessionHandler public function __construct( private readonly OrderRepositoryInterface $orderRepository, private readonly GatewayInterface $gateway, + private readonly InitializeApplePayPaymentFactoryInterface $initializeApplePayPaymentFactory, ) { } @@ -26,7 +27,7 @@ public function __invoke(InitializeApplePaySession $command): InitializeApplePay $this->verifyOrderExist($command->orderToken); $this->gateway->execute( - new InitializeApplePayPayment( + $this->initializeApplePayPaymentFactory->createNewWithModelAndOutput( new ArrayObject([ 'domainName' => $command->domainName, 'displayName' => $command->displayName, diff --git a/src/Payum/Factory/InitializeApplePayPaymentFactory.php b/src/Payum/Factory/InitializeApplePayPaymentFactory.php new file mode 100644 index 00000000..d0c35e5e --- /dev/null +++ b/src/Payum/Factory/InitializeApplePayPaymentFactory.php @@ -0,0 +1,16 @@ +createTestSubject(); + + $request = $factory->createNewWithModelAndOutput($model, $output); + + $this->assertInstanceOf(InitializeApplePayPayment::class, $request); + $this->assertSame($model, $request->getModel()); + $this->assertSame($output, $request->getOutput()); + } + + private function createTestSubject(): InitializeApplePayPaymentFactoryInterface + { + return new InitializeApplePayPaymentFactory(); + } +} From dba4edfa02530f81478e72c2769bb31e86971e0e Mon Sep 17 00:00:00 2001 From: Jacob Tobiasz Date: Tue, 22 Oct 2024 11:25:05 +0200 Subject: [PATCH 13/14] Fix the `InitializeApplePaySessionHandler` test --- .../InitializeApplePaySessionHandlerTest.php | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/tests/Unit/Api/Command/InitializeApplePaySessionHandlerTest.php b/tests/Unit/Api/Command/InitializeApplePaySessionHandlerTest.php index ea5ca7d6..57bdebe2 100644 --- a/tests/Unit/Api/Command/InitializeApplePaySessionHandlerTest.php +++ b/tests/Unit/Api/Command/InitializeApplePaySessionHandlerTest.php @@ -7,6 +7,7 @@ use CommerceWeavers\SyliusTpayPlugin\Api\Command\Exception\OrderCannotBeFoundException; use CommerceWeavers\SyliusTpayPlugin\Api\Command\InitializeApplePaySession; use CommerceWeavers\SyliusTpayPlugin\Api\Command\InitializeApplePaySessionHandler; +use CommerceWeavers\SyliusTpayPlugin\Payum\Factory\InitializeApplePayPaymentFactoryInterface; use CommerceWeavers\SyliusTpayPlugin\Payum\Request\Api\InitializeApplePayPayment; use Payum\Core\GatewayInterface; use PHPUnit\Framework\TestCase; @@ -24,10 +25,13 @@ final class InitializeApplePaySessionHandlerTest extends TestCase private GatewayInterface|ObjectProphecy $gateway; + private InitializeApplePayPaymentFactoryInterface|ObjectProphecy $initializeApplePayPaymentFactory; + protected function setUp(): void { $this->orderRepository = $this->prophesize(OrderRepositoryInterface::class); $this->gateway = $this->prophesize(GatewayInterface::class); + $this->initializeApplePayPaymentFactory = $this->prophesize(InitializeApplePayPaymentFactoryInterface::class); } public function test_it_throws_an_exception_if_an_order_with_the_given_token_does_not_exist(): void @@ -45,6 +49,10 @@ public function test_it_initializes_an_apple_pay_session(): void $order = $this->prophesize(OrderInterface::class); $this->orderRepository->findOneByTokenValue('t0k3n')->willReturn($order); + $this->initializeApplePayPaymentFactory + ->createNewWithModelAndOutput(Argument::any(), Argument::any()) + ->will(fn (array $args) => new InitializeApplePayPayment($args[0], $args[1])) + ; $this->gateway->execute(Argument::that(function (InitializeApplePayPayment $request): bool { $request->getOutput()->replace([ @@ -73,6 +81,10 @@ private function createCommand(): InitializeApplePaySession private function createTestSubject(): InitializeApplePaySessionHandler { - return new InitializeApplePaySessionHandler($this->orderRepository->reveal(), $this->gateway->reveal()); + return new InitializeApplePaySessionHandler( + $this->orderRepository->reveal(), + $this->gateway->reveal(), + $this->initializeApplePayPaymentFactory->reveal(), + ); } } From 6cb2d14779a72319d3014cdf26924281928ac9e3 Mon Sep 17 00:00:00 2001 From: Jacob Tobiasz Date: Tue, 22 Oct 2024 11:40:55 +0200 Subject: [PATCH 14/14] Allow to pass Apple Pay session token on the demo page --- .../templates/apple_pay_demo.html.twig | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/tests/Application/templates/apple_pay_demo.html.twig b/tests/Application/templates/apple_pay_demo.html.twig index ccc301a4..a7d51151 100644 --- a/tests/Application/templates/apple_pay_demo.html.twig +++ b/tests/Application/templates/apple_pay_demo.html.twig @@ -11,6 +11,10 @@ +
+ + +
@@ -47,8 +51,6 @@ } }]; - console.log(paymentMethodData); - // Define PaymentDetails const paymentDetails = { "total": { @@ -73,7 +75,14 @@ const request = new PaymentRequest(paymentMethodData, paymentDetails, paymentOptions); request.onmerchantvalidation = async event => { - await fetch('/en_US/tpay/apple-pay/init', { + const session = document.querySelector('[data-apple-pay-session-input]'); + + if (session.value) { + event.complete(JSON.parse(atob(session.value))); + return; + } + + await fetch('/tpay/apple-pay/init', { method: 'POST', headers: { 'Content-Type': 'application/json'