From 197248349297251774976dc8343a033ee49c24bc Mon Sep 17 00:00:00 2001 From: Jan Goralski Date: Fri, 30 Jan 2026 15:15:43 +0100 Subject: [PATCH 1/7] [API] Add an endpoint for getting mollie methods available for order --- config/routes.yaml | 4 + config/routes/api_shop.yaml | 5 + config/services.xml | 9 -- config/services/api.xml | 36 +++++++ src/Api/Controller/GetMollieMethodsAction.php | 95 +++++++++++++++++++ 5 files changed, 140 insertions(+), 9 deletions(-) create mode 100644 config/routes/api_shop.yaml create mode 100644 config/services/api.xml create mode 100644 src/Api/Controller/GetMollieMethodsAction.php diff --git a/config/routes.yaml b/config/routes.yaml index 94fe129b1..2d273ef0a 100644 --- a/config/routes.yaml +++ b/config/routes.yaml @@ -8,6 +8,10 @@ sylius_mollie_shop: requirements: _locale: ^[A-Za-z]{2,4}(_([A-Za-z]{4}|[0-9]{3}))?(_([A-Za-z]{2}|[0-9]{3}))?$ +sylius_mollie_api_shop: + prefix: "/%sylius.security.api_shop_route%" + resource: "routes/api_shop.yaml" + mollie_refund_integration: resource: "integration/refund-plugin/routes.php" prefix: /%sylius_admin.path_name% diff --git a/config/routes/api_shop.yaml b/config/routes/api_shop.yaml new file mode 100644 index 000000000..691066ce6 --- /dev/null +++ b/config/routes/api_shop.yaml @@ -0,0 +1,5 @@ +sylius_mollie_api_shop_order_mollie_methods: + path: orders/{tokenValue}/mollie-methods + methods: ['GET'] + defaults: + _controller: sylius_mollie.api.controller.get_mollie_methods diff --git a/config/services.xml b/config/services.xml index bd196177b..7798be7bf 100644 --- a/config/services.xml +++ b/config/services.xml @@ -23,15 +23,6 @@ - - - - - - %kernel.bundles% diff --git a/config/services/api.xml b/config/services/api.xml new file mode 100644 index 000000000..a219d6219 --- /dev/null +++ b/config/services/api.xml @@ -0,0 +1,36 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/src/Api/Controller/GetMollieMethodsAction.php b/src/Api/Controller/GetMollieMethodsAction.php new file mode 100644 index 000000000..9e40cd188 --- /dev/null +++ b/src/Api/Controller/GetMollieMethodsAction.php @@ -0,0 +1,95 @@ +orderRepository->findOneByTokenValue($tokenValue); + if (!$order instanceof OrderInterface) { + throw new NotFoundHttpException(sprintf('Order with token "%s" not found', $tokenValue)); + } + + $payment = $order->getLastPayment(); + if (!$payment instanceof PaymentInterface) { + throw new NotFoundHttpException('No payment found'); + } + + $paymentMethod = $payment->getMethod(); + if (!$paymentMethod instanceof PaymentMethodInterface) { + throw new NotFoundHttpException('No payment method found'); + } + + $gatewayConfig = $paymentMethod->getGatewayConfig(); + if (!$gatewayConfig instanceof GatewayConfigInterface) { + throw new NotFoundHttpException('No gateway config found'); + } + if (!$this->mollieGatewayFactoryChecker->isMollieGateway($gatewayConfig)) { + throw new BadRequestException('The payment method is not using Mollie'); + } + + $availableMethods = $this->molliePaymentsMethodResolver->resolve(); + + $data = $availableMethods['data'] ?? []; + $images = $availableMethods['image'] ?? []; + $paymentFees = $availableMethods['paymentFee'] ?? []; + $result = []; + foreach ($data as $id => $label) { + $result[] = [ + 'id' => $id, + 'label' => $label, + 'image' => $this->normalizeImage($images[$id] ?? null), + 'paymentFee' => $this->normalizePaymentFee($paymentFees[$id] ?? null), + ]; + } + + return new JsonResponse($result); + } + + private function normalizeImage(?string $image): ?string + { + if (null === $image || str_starts_with($image, 'https://') && str_contains($image, 'mollie.com')) { + return $image; + } + + return $this->imagineCacheManager->getBrowserPath($image, 'sylius_original'); + } + + private function normalizePaymentFee(mixed $fee): ?array + { + if ($fee instanceof PaymentSurchargeFeeInterface) { + return [ + 'type' => $fee->getType(), + 'fixedAmount' => $fee->getFixedAmount(), + 'percentage' => $fee->getPercentage(), + 'surchargeLimit' => $fee->getSurchargeLimit(), + ]; + } + + return null; + } +} From 60a8b2665e6602fffca1b3679e0ea2b841e020bd Mon Sep 17 00:00:00 2001 From: Jan Goralski Date: Mon, 2 Feb 2026 13:28:10 +0100 Subject: [PATCH 2/7] [API] Cover selecting inner method, creating payment in Mollie and related processing --- config/routes/api_shop.yaml | 6 + config/services/api.xml | 18 ++ .../Controller/SelectMollieMethodAction.php | 281 ++++++++++++++++++ 3 files changed, 305 insertions(+) create mode 100644 src/Api/Controller/SelectMollieMethodAction.php diff --git a/config/routes/api_shop.yaml b/config/routes/api_shop.yaml index 691066ce6..233218df9 100644 --- a/config/routes/api_shop.yaml +++ b/config/routes/api_shop.yaml @@ -3,3 +3,9 @@ sylius_mollie_api_shop_order_mollie_methods: methods: ['GET'] defaults: _controller: sylius_mollie.api.controller.get_mollie_methods + +sylius_mollie_api_shop_order_mollie_select_method: + path: orders/{tokenValue}/mollie-methods + methods: ['POST'] + defaults: + _controller: sylius_mollie.api.controller.select_mollie_method diff --git a/config/services/api.xml b/config/services/api.xml index a219d6219..90f4d81d4 100644 --- a/config/services/api.xml +++ b/config/services/api.xml @@ -32,5 +32,23 @@ + + + + + + + + + + + + + + + + + + diff --git a/src/Api/Controller/SelectMollieMethodAction.php b/src/Api/Controller/SelectMollieMethodAction.php new file mode 100644 index 000000000..3ba26a9de --- /dev/null +++ b/src/Api/Controller/SelectMollieMethodAction.php @@ -0,0 +1,281 @@ +orderRepository->findOneByTokenValue($tokenValue); + + if (!$order instanceof OrderInterface) { + throw new NotFoundHttpException(sprintf('Order with token "%s" not found', $tokenValue)); + } + + $data = json_decode($request->getContent(), true); + $methodId = $data['methodId'] ?? null; + + if (!$methodId) { + throw new BadRequestHttpException('The `methodId` is required'); + } + + $payment = $order->getLastPayment(); + if (!$payment instanceof PaymentInterface) { + throw new NotFoundHttpException('No payment found'); + } + + $paymentMethod = $payment->getMethod(); + if (!$paymentMethod instanceof PaymentMethodInterface) { + throw new NotFoundHttpException('No payment method found'); + } + + $gatewayConfig = $paymentMethod->getGatewayConfig(); + if (!$gatewayConfig instanceof GatewayConfigInterface) { + throw new NotFoundHttpException('No gateway config found'); + } + + if (!$this->mollieGatewayFactoryChecker->isMollieGateway($gatewayConfig)) { + throw new BadRequestException('The payment method is not using Mollie'); + } + + $mollieApiClient = $this->apiClientKeyResolver->getClientWithKey($order); + + $customerMollieId = null; + $isSubscription = $gatewayConfig->getFactoryName() === MollieSubscriptionGatewayFactory::FACTORY_NAME; + + if ($isSubscription) { + $customerMollieId = $this->findOrCreateMollieCustomer($mollieApiClient, $order); + } + + $paymentData = $this->createPaymentData( + $order, + $payment, + $gatewayConfig, + $methodId, + $isSubscription, + $customerMollieId, + ); + + try { + $molliePayment = $mollieApiClient->payments->create($paymentData); + + $checkoutUrl = $molliePayment->_links->checkout->href ?? null; + if (!$checkoutUrl) { + $this->logger->addNegativeLog(sprintf('No checkout URL returned from Mollie for order %s', $order->getNumber())); + + return new JsonResponse( + ['error' => 'No checkout URL returned from Mollie'], + Response::HTTP_INTERNAL_SERVER_ERROR, + ); + } + } catch (ApiException $exception) { + $this->logger->addNegativeLog(sprintf( + 'Creating payment in Mollie failed for order %s with: %s', + $order->getNumber(), + $exception->getMessage(), + )); + + return new JsonResponse( + ['error' => 'Creating payment in Mollie failed.'], + Response::HTTP_INTERNAL_SERVER_ERROR, + ); + } + + $details = [ + 'molliePaymentMethods' => $methodId, + 'payment_mollie_id' => $molliePayment->id, + 'cartToken' => null, + 'saveCardInfo' => '0', + 'useSavedCards' => '0', + 'webhookUrl' => $paymentData['webhookUrl'], + 'backurl' => $paymentData['redirectUrl'], + ]; + + if ($isSubscription) { + $details['customer_mollie_id'] = $customerMollieId; + } + + $payment->setDetails($details); + + if ($isSubscription && $order instanceof MollieOrderInterface) { + $this->createInternalSubscriptions($order, $paymentData, $customerMollieId); + } + + $this->entityManager->flush(); + + return new JsonResponse([ + 'success' => true, + 'methodId' => $methodId, + 'checkoutUrl' => $checkoutUrl, + 'paymentId' => $molliePayment->id, + ]); + } + + private function createPaymentData( + OrderInterface $order, + PaymentInterface $payment, + GatewayConfigInterface $gatewayConfig, + string $methodId, + bool $isSubscription, + ?string $customerMollieId, + ): array { + $divisor = $this->divisorProvider->getDivisor(); + + $methodConfig = $this->mollieGatewayConfigRepository->findOneActiveByGatewayNameAndMethod( + $gatewayConfig->getGatewayName(), + $methodId, + ); + if (null === $methodConfig) { + throw new BadRequestHttpException(sprintf('Mollie method "%s" cannot be selected', $methodId)); + } + + $webhookUrl = $this->router->generate( + 'sylius_mollie_shop_payment_webhook', + ['_locale' => $order->getLocaleCode() ?? 'en_US'], + UrlGeneratorInterface::ABSOLUTE_URL, + ); + + $redirectUrl = $this->router->generate( + 'sylius_mollie_api_shop_order_mollie_payment_status', + ['tokenValue' => $order->getTokenValue()], + UrlGeneratorInterface::ABSOLUTE_URL, + ); + $paymentData = [ + 'method' => $methodId, + 'amount' => [ + 'currency' => $payment->getCurrencyCode(), + 'value' => $this->intToStringConverter->convertIntToString($payment->getAmount(), $divisor), + ], + 'description' => $this->paymentDescriptionProvider->getPaymentDescription($payment, $methodConfig, $order), + 'redirectUrl' => $redirectUrl, + 'webhookUrl' => $webhookUrl, + 'metadata' => [ + 'order_id' => $order->getId(), + 'customer_id' => $order->getCustomer()?->getId(), + 'molliePaymentMethods' => $methodId, + ], + ]; + + $converted = $this->orderConverter->convert($order, $paymentData, $divisor, $methodConfig); + $paymentData['billingAddress'] = $converted['billingAddress']; + $paymentData['shippingAddress'] = $converted['shippingAddress']; + $paymentData['lines'] = array_map( + static fn (array $line): array => [ + 'description' => $line['name'] ?? '', + 'type' => $line['type'] ?? 'physical', + 'quantity' => $line['quantity'], + 'unitPrice' => $line['unitPrice'], + 'totalAmount' => $line['totalAmount'], + 'vatRate' => $line['vatRate'], + 'vatAmount' => $line['vatAmount'], + ], + $converted['lines'], + ); + + $locale = $this->paymentLocaleResolver->resolveFromOrder($order); + + if ($locale !== null) { + $paymentData['locale'] = $locale; + } + + if ($isSubscription) { + $paymentData['customerId'] = $customerMollieId; + $paymentData['sequenceType'] = 'first'; + $paymentData['metadata']['sequenceType'] = 'first'; + } + + return $paymentData; + } + + private function findOrCreateMollieCustomer(MollieApiClient $mollieApiClient, OrderInterface $order): string + { + $email = $order->getCustomer()?->getEmail(); + + /** @var ?MollieCustomer $customer */ + $customer = $this->mollieCustomerRepository->findOneBy(['email' => $email]); + if (null === $customer) { + $customer = new MollieCustomer(); + $customer->setEmail($email); + } + + if (null === $customer->getProfileId()) { + $customerMollie = $mollieApiClient->customers->create([ + 'name' => $order->getCustomer()?->getFullName(), + 'email' => $email, + ]); + $customer->setProfileId($customerMollie->id); + + $this->mollieCustomerRepository->add($customer); + } + + return $customer->getProfileId(); + } + + private function createInternalSubscriptions( + MollieOrderInterface $order, + array $paymentData, + ?string $customerMollieId, + ): void { + foreach ($order->getRecurringItems() as $item) { + $subscription = $this->subscriptionFactory->createFromFirstOrderWithOrderItemAndPaymentConfiguration( + $order, + $item, + $paymentData, + ); + $subscription->getSubscriptionConfiguration()->setCustomerId($customerMollieId); + $this->subscriptionRepository->add($subscription); + } + } +} From e7751114cebe0a4856e3aba3ef75b69c9ad6ec32 Mon Sep 17 00:00:00 2001 From: Jan Goralski Date: Mon, 2 Feb 2026 13:54:22 +0100 Subject: [PATCH 3/7] [API] Cover updating Sylius payment state --- config/routes/api_shop.yaml | 6 ++ config/services/api.xml | 8 ++ src/Api/Controller/GetMollieStatusAction.php | 89 ++++++++++++++++++++ 3 files changed, 103 insertions(+) create mode 100644 src/Api/Controller/GetMollieStatusAction.php diff --git a/config/routes/api_shop.yaml b/config/routes/api_shop.yaml index 233218df9..b069877be 100644 --- a/config/routes/api_shop.yaml +++ b/config/routes/api_shop.yaml @@ -9,3 +9,9 @@ sylius_mollie_api_shop_order_mollie_select_method: methods: ['POST'] defaults: _controller: sylius_mollie.api.controller.select_mollie_method + +sylius_mollie_api_shop_order_mollie_payment_status: + path: orders/{tokenValue}/mollie-status + methods: ['GET'] + defaults: + _controller: sylius_mollie.api.controller.get_mollie_status diff --git a/config/services/api.xml b/config/services/api.xml index 90f4d81d4..ba18f1a4c 100644 --- a/config/services/api.xml +++ b/config/services/api.xml @@ -50,5 +50,13 @@ + + + + + + + + diff --git a/src/Api/Controller/GetMollieStatusAction.php b/src/Api/Controller/GetMollieStatusAction.php new file mode 100644 index 000000000..819276be3 --- /dev/null +++ b/src/Api/Controller/GetMollieStatusAction.php @@ -0,0 +1,89 @@ +orderRepository->findOneByTokenValue($tokenValue); + if (null === $order) { + throw new NotFoundHttpException(sprintf('Order with token "%s" not found', $tokenValue)); + } + + $payment = $order->getLastPayment(); + if (null === $payment) { + throw new NotFoundHttpException('No payment found for this order'); + } + + $details = $payment->getDetails(); + $molliePaymentId = $details['payment_mollie_id'] ?? null; + if (null === $molliePaymentId) { + $this->logger->addNegativeLog(sprintf( + 'No Mollie payment ID found in payment details of order with token: %s', + $tokenValue, + )); + + throw new NotFoundHttpException('No payment found for this order'); + } + + try { + $mollieApiClient = $this->apiKeyResolver->getClientWithKey($order); + $molliePayment = $mollieApiClient->payments->get($molliePaymentId); + $mollieStatus = $molliePayment->status; + } catch (\Exception $exception) { + $this->logger->addNegativeLog(sprintf('Error fetching Mollie payment status: %s', $exception->getMessage())); + + return new JsonResponse([ + 'error' => 'Could not retrieve Mollie payment status', + ], Response::HTTP_INTERNAL_SERVER_ERROR); + } + + $transition = $this->mapMolliePaymentStatusToTransition($mollieStatus); + + if (null !== $transition && $this->stateMachine->can($payment, PaymentTransitions::GRAPH, $transition)) { + $this->stateMachine->apply($payment, PaymentTransitions::GRAPH, $transition); + $this->entityManager->flush(); + } + + return new JsonResponse([ + 'orderNumber' => $order->getNumber(), + 'orderToken' => $tokenValue, + 'paymentState' => $payment->getState(), + ]); + } + + private function mapMolliePaymentStatusToTransition(string $status): ?string + { + return match ($status) { + MolliePaymentStatus::STATUS_PENDING, MolliePaymentStatus::STATUS_OPEN => PaymentTransitions::TRANSITION_PROCESS, + MolliePaymentStatus::STATUS_AUTHORIZED => PaymentTransitions::TRANSITION_AUTHORIZE, + MolliePaymentStatus::STATUS_PAID => PaymentTransitions::TRANSITION_COMPLETE, + MolliePaymentStatus::STATUS_CANCELED => PaymentTransitions::TRANSITION_CANCEL, + MolliePaymentStatus::STATUS_EXPIRED, MolliePaymentStatus::STATUS_FAILED => PaymentTransitions::TRANSITION_FAIL, + default => null, + }; + } +} From 7a305f8e298a5c2746e0f84be236df328b5a4b13 Mon Sep 17 00:00:00 2001 From: Jan Goralski Date: Mon, 2 Feb 2026 14:07:54 +0100 Subject: [PATCH 4/7] [Maintenance] Fix ecs and stan --- src/Api/Controller/GetMollieMethodsAction.php | 45 +++++++++++++------ src/Api/Controller/GetMollieStatusAction.php | 12 ++++- .../Controller/SelectMollieMethodAction.php | 18 +++++++- 3 files changed, 58 insertions(+), 17 deletions(-) diff --git a/src/Api/Controller/GetMollieMethodsAction.php b/src/Api/Controller/GetMollieMethodsAction.php index 9e40cd188..e2d36c247 100644 --- a/src/Api/Controller/GetMollieMethodsAction.php +++ b/src/Api/Controller/GetMollieMethodsAction.php @@ -1,15 +1,24 @@ molliePaymentsMethodResolver->resolve(); - $data = $availableMethods['data'] ?? []; - $images = $availableMethods['image'] ?? []; - $paymentFees = $availableMethods['paymentFee'] ?? []; + $data = $availableMethods['data']; + $images = $availableMethods['image']; + $paymentFees = $availableMethods['paymentFee']; $result = []; foreach ($data as $id => $label) { $result[] = [ @@ -70,15 +79,14 @@ public function __invoke(string $tokenValue): JsonResponse return new JsonResponse($result); } - private function normalizeImage(?string $image): ?string - { - if (null === $image || str_starts_with($image, 'https://') && str_contains($image, 'mollie.com')) { - return $image; - } - - return $this->imagineCacheManager->getBrowserPath($image, 'sylius_original'); - } - + /** + * @return array{ + * type: string|null, + * fixedAmount: float|null, + * percentage: float|null, + * surchargeLimit: float|null, + * }|null + */ private function normalizePaymentFee(mixed $fee): ?array { if ($fee instanceof PaymentSurchargeFeeInterface) { @@ -92,4 +100,13 @@ private function normalizePaymentFee(mixed $fee): ?array return null; } + + private function normalizeImage(?string $image): ?string + { + if (null === $image || str_starts_with($image, 'https://') && str_contains($image, 'mollie.com')) { + return $image; + } + + return $this->imagineCacheManager->getBrowserPath($image, 'sylius_original'); + } } diff --git a/src/Api/Controller/GetMollieStatusAction.php b/src/Api/Controller/GetMollieStatusAction.php index 819276be3..ee29218a9 100644 --- a/src/Api/Controller/GetMollieStatusAction.php +++ b/src/Api/Controller/GetMollieStatusAction.php @@ -1,5 +1,14 @@ orderRepository->findOneByTokenValue($tokenValue); - if (null === $order) { + if (!$order instanceof OrderInterface) { throw new NotFoundHttpException(sprintf('Order with token "%s" not found', $tokenValue)); } diff --git a/src/Api/Controller/SelectMollieMethodAction.php b/src/Api/Controller/SelectMollieMethodAction.php index 3ba26a9de..0503b4375 100644 --- a/src/Api/Controller/SelectMollieMethodAction.php +++ b/src/Api/Controller/SelectMollieMethodAction.php @@ -1,12 +1,20 @@ orderRepository->findOneByTokenValue($tokenValue); - if (!$order instanceof OrderInterface) { throw new NotFoundHttpException(sprintf('Order with token "%s" not found', $tokenValue)); } @@ -163,6 +171,9 @@ public function __invoke(string $tokenValue, Request $request): JsonResponse ]); } + /** + * @return array + */ private function createPaymentData( OrderInterface $order, PaymentInterface $payment, @@ -263,6 +274,9 @@ private function findOrCreateMollieCustomer(MollieApiClient $mollieApiClient, Or return $customer->getProfileId(); } + /** + * @param array $paymentData + */ private function createInternalSubscriptions( MollieOrderInterface $order, array $paymentData, From f2df19046931d98c5fcb82f5f9e4c993cf351251 Mon Sep 17 00:00:00 2001 From: Jan Goralski Date: Mon, 2 Feb 2026 14:25:03 +0100 Subject: [PATCH 5/7] [API] Add custom payment-related endpoints to open api docs --- config/services/api.xml | 6 + src/Api/Controller/GetMollieStatusAction.php | 1 - .../Controller/SelectMollieMethodAction.php | 2 - .../OpenApi/MollieDocumentationModifier.php | 179 ++++++++++++++++++ 4 files changed, 185 insertions(+), 3 deletions(-) create mode 100644 src/Api/OpenApi/MollieDocumentationModifier.php diff --git a/config/services/api.xml b/config/services/api.xml index ba18f1a4c..f6aefa3b3 100644 --- a/config/services/api.xml +++ b/config/services/api.xml @@ -58,5 +58,11 @@ + + + %sylius.security.api_shop_route% + + + diff --git a/src/Api/Controller/GetMollieStatusAction.php b/src/Api/Controller/GetMollieStatusAction.php index ee29218a9..c702807b0 100644 --- a/src/Api/Controller/GetMollieStatusAction.php +++ b/src/Api/Controller/GetMollieStatusAction.php @@ -79,7 +79,6 @@ public function __invoke(string $tokenValue): JsonResponse } return new JsonResponse([ - 'orderNumber' => $order->getNumber(), 'orderToken' => $tokenValue, 'paymentState' => $payment->getState(), ]); diff --git a/src/Api/Controller/SelectMollieMethodAction.php b/src/Api/Controller/SelectMollieMethodAction.php index 0503b4375..fd191d2bf 100644 --- a/src/Api/Controller/SelectMollieMethodAction.php +++ b/src/Api/Controller/SelectMollieMethodAction.php @@ -164,10 +164,8 @@ public function __invoke(string $tokenValue, Request $request): JsonResponse $this->entityManager->flush(); return new JsonResponse([ - 'success' => true, 'methodId' => $methodId, 'checkoutUrl' => $checkoutUrl, - 'paymentId' => $molliePayment->id, ]); } diff --git a/src/Api/OpenApi/MollieDocumentationModifier.php b/src/Api/OpenApi/MollieDocumentationModifier.php new file mode 100644 index 000000000..55e0f39d6 --- /dev/null +++ b/src/Api/OpenApi/MollieDocumentationModifier.php @@ -0,0 +1,179 @@ +getPaths(); + $schemas = $docs->getComponents()->getSchemas(); + + $schemas = $this->addSchemas($schemas); + $this->addPaths($paths); + + return $docs + ->withPaths($paths) + ->withComponents($docs->getComponents()->withSchemas($schemas)) + ; + } + + private function addPaths(Paths $paths): void + { + $tokenParameter = new Parameter( + name: 'tokenValue', + in: 'path', + description: 'The token of the order', + required: true, + schema: ['type' => 'string'], + ); + + $methodsPath = sprintf('%s/orders/{tokenValue}/mollie-methods', $this->shopApiRoute); + $paths->addPath($methodsPath, new PathItem( + ref: 'MollieMethods', + get: new Operation( + operationId: 'sylius_mollie_api_shop_order_mollie_methods', + tags: ['Mollie'], + responses: [ + Response::HTTP_OK => new ResponseModel( + description: 'List of available Mollie payment methods', + content: new \ArrayObject([ + 'application/json' => [ + 'schema' => [ + 'type' => 'array', + 'items' => ['$ref' => '#/components/schemas/MollieMethod'], + ], + ], + ]), + ), + ], + summary: 'Get available Mollie payment methods for an order', + parameters: [$tokenParameter], + ), + post: new Operation( + operationId: 'sylius_mollie_api_shop_order_mollie_select_method', + tags: ['Mollie'], + responses: [ + Response::HTTP_OK => new ResponseModel( + description: 'Mollie payment created successfully', + content: new \ArrayObject([ + 'application/json' => [ + 'schema' => ['$ref' => '#/components/schemas/MollieSelectMethodResponse'], + ], + ]), + ), + ], + summary: 'Select a Mollie payment method and create a payment', + parameters: [$tokenParameter], + requestBody: new RequestBody( + content: new \ArrayObject([ + 'application/json' => [ + 'schema' => ['$ref' => '#/components/schemas/MollieSelectMethodRequest'], + ], + ]), + required: true, + ), + ), + )); + + $statusPath = sprintf('%s/orders/{tokenValue}/mollie-status', $this->shopApiRoute); + $paths->addPath($statusPath, new PathItem( + ref: 'MolliePaymentStatus', + get: new Operation( + operationId: 'sylius_mollie_api_shop_order_mollie_payment_status', + tags: ['Mollie'], + responses: [ + Response::HTTP_OK => new ResponseModel( + description: 'Current payment status', + content: new \ArrayObject([ + 'application/json' => [ + 'schema' => ['$ref' => '#/components/schemas/MolliePaymentStatus'], + ], + ]), + ), + ], + summary: 'Get Mollie payment status and update Sylius payment state', + parameters: [$tokenParameter], + ), + )); + } + + /** + * @param array|\ArrayObject $schemas + * + * @return array|\ArrayObject + */ + private function addSchemas(array|\ArrayObject $schemas): array|\ArrayObject + { + $schemas['MollieMethod'] = [ + 'type' => 'object', + 'properties' => [ + 'id' => ['type' => 'string', 'example' => 'klarna'], + 'label' => ['type' => 'string', 'example' => 'Klarna'], + 'image' => ['type' => 'string', 'nullable' => true, 'example' => 'https://www.mollie.com/external/icons/payment-methods/klarna.svg'], + 'paymentFee' => [ + 'type' => 'object', + 'nullable' => true, + 'properties' => [ + 'type' => ['type' => 'string', 'nullable' => true], + 'fixedAmount' => ['type' => 'number', 'nullable' => true], + 'percentage' => ['type' => 'number', 'nullable' => true], + 'surchargeLimit' => ['type' => 'number', 'nullable' => true], + ], + ], + ], + ]; + + $schemas['MollieSelectMethodRequest'] = [ + 'type' => 'object', + 'required' => ['methodId'], + 'properties' => [ + 'methodId' => ['type' => 'string', 'example' => 'ideal'], + ], + ]; + + $schemas['MollieSelectMethodResponse'] = [ + 'type' => 'object', + 'properties' => [ + 'methodId' => ['type' => 'string'], + 'checkoutUrl' => ['type' => 'string'], + ], + ]; + + $schemas['MolliePaymentStatus'] = [ + 'type' => 'object', + 'properties' => [ + 'orderToken' => ['type' => 'string'], + 'paymentState' => ['type' => 'string'], + ], + ]; + + return $schemas; + } +} From 0051d0289390b9511d978c0082b3d1f13a371a1b Mon Sep 17 00:00:00 2001 From: Jan Goralski Date: Mon, 2 Feb 2026 15:43:18 +0100 Subject: [PATCH 6/7] [API] Require passing `backUrl` to select action --- src/Api/Controller/GetMollieStatusAction.php | 1 - src/Api/Controller/SelectMollieMethodAction.php | 16 +++++++--------- src/Api/OpenApi/MollieDocumentationModifier.php | 4 ++-- 3 files changed, 9 insertions(+), 12 deletions(-) diff --git a/src/Api/Controller/GetMollieStatusAction.php b/src/Api/Controller/GetMollieStatusAction.php index c702807b0..53d4a72a1 100644 --- a/src/Api/Controller/GetMollieStatusAction.php +++ b/src/Api/Controller/GetMollieStatusAction.php @@ -79,7 +79,6 @@ public function __invoke(string $tokenValue): JsonResponse } return new JsonResponse([ - 'orderToken' => $tokenValue, 'paymentState' => $payment->getState(), ]); } diff --git a/src/Api/Controller/SelectMollieMethodAction.php b/src/Api/Controller/SelectMollieMethodAction.php index fd191d2bf..d73c6fbdd 100644 --- a/src/Api/Controller/SelectMollieMethodAction.php +++ b/src/Api/Controller/SelectMollieMethodAction.php @@ -73,10 +73,11 @@ public function __invoke(string $tokenValue, Request $request): JsonResponse } $data = json_decode($request->getContent(), true); - $methodId = $data['methodId'] ?? null; - if (!$methodId) { - throw new BadRequestHttpException('The `methodId` is required'); + $methodId = $data['methodId'] ?? null; + $backUrl = $data['backUrl'] ?? null; + if (!$methodId || !$backUrl) { + throw new BadRequestHttpException('The `methodId` and `backUrl` are required'); } $payment = $order->getLastPayment(); @@ -113,6 +114,7 @@ public function __invoke(string $tokenValue, Request $request): JsonResponse $gatewayConfig, $methodId, $isSubscription, + $backUrl, $customerMollieId, ); @@ -178,6 +180,7 @@ private function createPaymentData( GatewayConfigInterface $gatewayConfig, string $methodId, bool $isSubscription, + string $backUrl, ?string $customerMollieId, ): array { $divisor = $this->divisorProvider->getDivisor(); @@ -196,11 +199,6 @@ private function createPaymentData( UrlGeneratorInterface::ABSOLUTE_URL, ); - $redirectUrl = $this->router->generate( - 'sylius_mollie_api_shop_order_mollie_payment_status', - ['tokenValue' => $order->getTokenValue()], - UrlGeneratorInterface::ABSOLUTE_URL, - ); $paymentData = [ 'method' => $methodId, 'amount' => [ @@ -208,7 +206,7 @@ private function createPaymentData( 'value' => $this->intToStringConverter->convertIntToString($payment->getAmount(), $divisor), ], 'description' => $this->paymentDescriptionProvider->getPaymentDescription($payment, $methodConfig, $order), - 'redirectUrl' => $redirectUrl, + 'redirectUrl' => $backUrl, 'webhookUrl' => $webhookUrl, 'metadata' => [ 'order_id' => $order->getId(), diff --git a/src/Api/OpenApi/MollieDocumentationModifier.php b/src/Api/OpenApi/MollieDocumentationModifier.php index 55e0f39d6..1b212d58d 100644 --- a/src/Api/OpenApi/MollieDocumentationModifier.php +++ b/src/Api/OpenApi/MollieDocumentationModifier.php @@ -152,9 +152,10 @@ private function addSchemas(array|\ArrayObject $schemas): array|\ArrayObject $schemas['MollieSelectMethodRequest'] = [ 'type' => 'object', - 'required' => ['methodId'], + 'required' => ['methodId', 'backUrl'], 'properties' => [ 'methodId' => ['type' => 'string', 'example' => 'ideal'], + 'backUrl' => ['type' => 'string', 'example' => 'https://example.com/return-here-after-payment'], ], ]; @@ -169,7 +170,6 @@ private function addSchemas(array|\ArrayObject $schemas): array|\ArrayObject $schemas['MolliePaymentStatus'] = [ 'type' => 'object', 'properties' => [ - 'orderToken' => ['type' => 'string'], 'paymentState' => ['type' => 'string'], ], ]; From 420063995836b6878d83cd262a11d97d1f9aae72 Mon Sep 17 00:00:00 2001 From: Jan Goralski Date: Mon, 2 Feb 2026 16:17:13 +0100 Subject: [PATCH 7/7] [API] Allow getting Mollie methods for carts --- config/services/api.xml | 2 +- config/services/repository.xml | 5 +++ src/Api/Controller/GetMollieMethodsAction.php | 9 ++-- .../Controller/SelectMollieMethodAction.php | 4 +- .../OrderByTokenForAvailableMethodsQuery.php | 42 +++++++++++++++++++ ...TokenForAvailableMethodsQueryInterface.php | 21 ++++++++++ 6 files changed, 75 insertions(+), 8 deletions(-) create mode 100644 src/Repository/Query/OrderByTokenForAvailableMethodsQuery.php create mode 100644 src/Repository/Query/OrderByTokenForAvailableMethodsQueryInterface.php diff --git a/config/services/api.xml b/config/services/api.xml index f6aefa3b3..c6015d8ad 100644 --- a/config/services/api.xml +++ b/config/services/api.xml @@ -27,7 +27,7 @@ - + diff --git a/config/services/repository.xml b/config/services/repository.xml index 8cdd4031c..034983e12 100644 --- a/config/services/repository.xml +++ b/config/services/repository.xml @@ -26,5 +26,10 @@ + + + + + diff --git a/src/Api/Controller/GetMollieMethodsAction.php b/src/Api/Controller/GetMollieMethodsAction.php index e2d36c247..2388a2acd 100644 --- a/src/Api/Controller/GetMollieMethodsAction.php +++ b/src/Api/Controller/GetMollieMethodsAction.php @@ -16,11 +16,10 @@ use Liip\ImagineBundle\Imagine\Cache\CacheManager; use Sylius\Component\Core\Model\PaymentInterface; use Sylius\Component\Core\Model\PaymentMethodInterface; -use Sylius\Component\Core\Repository\OrderRepositoryInterface; use Sylius\MolliePlugin\Entity\GatewayConfigInterface; -use Sylius\MolliePlugin\Entity\OrderInterface; use Sylius\MolliePlugin\Entity\PaymentSurchargeFeeInterface; use Sylius\MolliePlugin\Payum\Checker\MollieGatewayFactoryCheckerInterface; +use Sylius\MolliePlugin\Repository\Query\OrderByTokenForAvailableMethodsQueryInterface; use Sylius\MolliePlugin\Resolver\MolliePaymentsMethodResolver; use Symfony\Component\HttpFoundation\Exception\BadRequestException; use Symfony\Component\HttpFoundation\JsonResponse; @@ -29,7 +28,7 @@ final class GetMollieMethodsAction { public function __construct( - private readonly OrderRepositoryInterface $orderRepository, + private readonly OrderByTokenForAvailableMethodsQueryInterface $orderByTokenForAvailableMethodsQuery, private readonly MollieGatewayFactoryCheckerInterface $mollieGatewayFactoryChecker, private readonly MolliePaymentsMethodResolver $molliePaymentsMethodResolver, private readonly CacheManager $imagineCacheManager, @@ -38,8 +37,8 @@ public function __construct( public function __invoke(string $tokenValue): JsonResponse { - $order = $this->orderRepository->findOneByTokenValue($tokenValue); - if (!$order instanceof OrderInterface) { + $order = $this->orderByTokenForAvailableMethodsQuery->getOrder($tokenValue); + if (null === $order) { throw new NotFoundHttpException(sprintf('Order with token "%s" not found', $tokenValue)); } diff --git a/src/Api/Controller/SelectMollieMethodAction.php b/src/Api/Controller/SelectMollieMethodAction.php index d73c6fbdd..ef1463e76 100644 --- a/src/Api/Controller/SelectMollieMethodAction.php +++ b/src/Api/Controller/SelectMollieMethodAction.php @@ -76,7 +76,7 @@ public function __invoke(string $tokenValue, Request $request): JsonResponse $methodId = $data['methodId'] ?? null; $backUrl = $data['backUrl'] ?? null; - if (!$methodId || !$backUrl) { + if (empty($methodId) || empty($backUrl)) { throw new BadRequestHttpException('The `methodId` and `backUrl` are required'); } @@ -159,7 +159,7 @@ public function __invoke(string $tokenValue, Request $request): JsonResponse $payment->setDetails($details); - if ($isSubscription && $order instanceof MollieOrderInterface) { + if ($isSubscription) { $this->createInternalSubscriptions($order, $paymentData, $customerMollieId); } diff --git a/src/Repository/Query/OrderByTokenForAvailableMethodsQuery.php b/src/Repository/Query/OrderByTokenForAvailableMethodsQuery.php new file mode 100644 index 000000000..209cf9285 --- /dev/null +++ b/src/Repository/Query/OrderByTokenForAvailableMethodsQuery.php @@ -0,0 +1,42 @@ +orderRepository->createQueryBuilder('o') + ->andWhere('o.tokenValue = :tokenValue') + ->andWhere('o.state IN (:states)') + ->setParameter('tokenValue', $tokenValue) + ->setParameter('states', [ + OrderInterface::STATE_NEW, + OrderInterface::STATE_CART, + ]) + ->getQuery() + ->getOneOrNullResult() + ; + } +} diff --git a/src/Repository/Query/OrderByTokenForAvailableMethodsQueryInterface.php b/src/Repository/Query/OrderByTokenForAvailableMethodsQueryInterface.php new file mode 100644 index 000000000..2359a0188 --- /dev/null +++ b/src/Repository/Query/OrderByTokenForAvailableMethodsQueryInterface.php @@ -0,0 +1,21 @@ +