diff --git a/src/new/var/plugins/System/PSC/Mollie/Api/Base.php b/src/new/var/plugins/System/PSC/Mollie/Api/Base.php new file mode 100644 index 000000000..4216bed3e --- /dev/null +++ b/src/new/var/plugins/System/PSC/Mollie/Api/Base.php @@ -0,0 +1,62 @@ +client = $client; + } + + public function postV2(string $url, array $data): array + { + $response = $this->client->request('POST', $this->baseUrl . $url, [ + 'headers' => $this->buildHeaders(), + 'json' => $data, + ]); + + return $response->toArray(); + } + + public function getV2(string $url): array + { + $response = $this->client->request('GET', $this->baseUrl . $url, [ + 'headers' => $this->buildHeaders(), + ]); + + return $response->toArray(); + } + + protected function buildHeaders(): array + { + return [ + 'Accept' => 'application/json', + 'Content-Type' => 'application/json', + 'Authorization' => 'Bearer ' . $this->apiKey, + ]; + } + + public function setShop(\PSC\Shop\EntityBundle\Document\Shop $shop): void + { + $this->shop = $shop; + } + + public function setApiKey(string $apiKey): void + { + $this->apiKey = $apiKey; + } +} diff --git a/src/new/var/plugins/System/PSC/Mollie/Api/CreatePayment.php b/src/new/var/plugins/System/PSC/Mollie/Api/CreatePayment.php new file mode 100644 index 000000000..4c5483bfe --- /dev/null +++ b/src/new/var/plugins/System/PSC/Mollie/Api/CreatePayment.php @@ -0,0 +1,84 @@ +postV2('/v2/payments', $this->buildData()); + } + + protected function buildData(): array + { + $basket = $_SESSION['Basket']; + + $amount = round($basket['brutto'] - round($basket['GutscheinAbzug'], 2), 2); + + $data = [ + 'amount' => [ + 'currency' => 'EUR', + 'value' => number_format($amount, 2, '.', ''), + ], + 'description' => $this->description !== '' ? $this->description : 'Bestellung', + 'redirectUrl' => $this->redirectUrl, + ]; + + if ($this->webhookUrl !== '') { + $data['webhookUrl'] = $this->webhookUrl; + } + + if ($this->orderReference !== null) { + $data['metadata'] = [ + 'order_reference' => $this->orderReference, + ]; + } + + if ($this->invoiceAddress !== null) { + $data['billingAddress'] = [ + 'givenName' => $this->invoiceAddress->getFirstname(), + 'familyName' => $this->invoiceAddress->getLastname(), + 'email' => $this->invoiceAddress->getEmail(), + 'streetAndNumber' => trim($this->invoiceAddress->getStreet() . ' ' . $this->invoiceAddress->getHouseNumber()), + 'postalCode' => $this->invoiceAddress->getZip(), + 'city' => $this->invoiceAddress->getCity(), + 'country' => $this->invoiceAddress->getCountry(), + ]; + } + + return $data; + } + + public function setInvoiceAddress(ContactAddress $invoiceAddress): void + { + $this->invoiceAddress = $invoiceAddress; + } + + public function setDescription(string $description): void + { + $this->description = $description; + } + + public function setRedirectUrl(string $redirectUrl): void + { + $this->redirectUrl = $redirectUrl; + } + + public function setWebhookUrl(string $webhookUrl): void + { + $this->webhookUrl = $webhookUrl; + } + + public function setOrderReference(?string $orderReference): void + { + $this->orderReference = $orderReference; + } +} diff --git a/src/new/var/plugins/System/PSC/Mollie/Api/GetPayment.php b/src/new/var/plugins/System/PSC/Mollie/Api/GetPayment.php new file mode 100644 index 000000000..49c9cf775 --- /dev/null +++ b/src/new/var/plugins/System/PSC/Mollie/Api/GetPayment.php @@ -0,0 +1,18 @@ +getV2('/v2/payments/' . urlencode($this->paymentId)); + } + + public function setPaymentId(string $paymentId): void + { + $this->paymentId = $paymentId; + } +} diff --git a/src/new/var/plugins/System/PSC/Mollie/Document/Mollie.php b/src/new/var/plugins/System/PSC/Mollie/Document/Mollie.php new file mode 100644 index 000000000..f0d204b6c --- /dev/null +++ b/src/new/var/plugins/System/PSC/Mollie/Document/Mollie.php @@ -0,0 +1,65 @@ +apiKey; + } + + public function setApiKey(?string $apiKey): void + { + $this->apiKey = $apiKey; + } + + public function getTestApiKey(): string + { + return (string)$this->testApiKey; + } + + public function setTestApiKey(?string $testApiKey): void + { + $this->testApiKey = $testApiKey; + } + + public function isProduction(): bool + { + return (bool)$this->production; + } + + public function setProduction(bool $production): void + { + $this->production = $production; + } + + /** + * Returns the API key matching the configured mode. + */ + public function getActiveApiKey(): string + { + return $this->isProduction() ? $this->getApiKey() : $this->getTestApiKey(); + } +} diff --git a/src/new/var/plugins/System/PSC/Mollie/Payment/Provider.php b/src/new/var/plugins/System/PSC/Mollie/Payment/Provider.php new file mode 100644 index 000000000..2a60b6311 --- /dev/null +++ b/src/new/var/plugins/System/PSC/Mollie/Payment/Provider.php @@ -0,0 +1,219 @@ +_formFactory = $formFactory; + $this->_entityManager = $entityManager; + $this->_doctrine_mongodb = $doctrine_mongodb; + $this->shopService = $shopService; + $this->eventManager = $eventManager; + $this->createPaymentService = $createPayment; + $this->getPaymentService = $getPayment; + } + + public function getName() + { + return 'Mollie'; + } + + public function getType() + { + return 'mollie'; + } + + public function saveDocument(Gatewaysettings $settings, Form $form) + { + $doc = new Mollie(); + + $doc->setApiKey($form->get('mollie')->get('apiKey')->getData()); + $doc->setTestApiKey($form->get('mollie')->get('testApiKey')->getData()); + $doc->setProduction((bool) $form->get('mollie')->get('production')->getData()); + + $settings->setGatewayDocument($doc); + + return $settings; + } + + public function getSubForm(Gatewaysettings $settings, FormBuilder $builder) + { + if (!$settings->getGatewayDocument()) { + $settings->setGatewayDocument(new Mollie()); + } + + $builder->add('production', CheckboxType::class, [ + 'label' => 'Production?', + 'required' => false, + ])->add('apiKey', TextType::class, [ + 'label' => 'Live API-Key', + 'required' => false, + ])->add('testApiKey', TextType::class, ['label' => 'Test API-Key', 'required' => false]); + + $builder->get('production')->setData($settings->getGatewayDocument()->isProduction()); + $builder->get('apiKey')->setData($settings->getGatewayDocument()->getApiKey()); + $builder->get('testApiKey')->setData($settings->getGatewayDocument()->getTestApiKey()); + + return $builder; + } + + public function getTemplate() + { + return '@PluginSystemPSCMollie/settings.html.twig'; + } + + public function handlePayment(Request $request) + { + $invoice = $request->get('invoiceAddress'); + + /** @var ContactAddress $invoiceAddress */ + $invoiceAddress = $this->_entityManager->getRepository(ContactAddress::class)->findOneBy(['uuid' => $invoice]); + + /** @var Mollie $settings */ + $settings = $this->getGatewaySettings()->getGatewayDocument(); + + $this->createPaymentService->setShop($this->shopDoc); + $this->createPaymentService->setApiKey($settings->getActiveApiKey()); + $this->createPaymentService->setInvoiceAddress($invoiceAddress); + $this->createPaymentService->setRedirectUrl( + $this->getHost() . '/basket/finish?Data=finish&token=' . $request->get('hash') . '&paymentGateway=mollie', + ); + + if ($this->isPubliclyReachable($this->getHost())) { + $this->createPaymentService->setWebhookUrl($this->getHost() . '/payment/notify/mollie'); + } + + try { + $response = $this->createPaymentService->call(); + } catch (\Exception $e) { + die($e->getMessage()); + return new RedirectResponse($this->getHost() . '/basket/finish?error=Mollie'); + } + + if (!isset($response['_links']['checkout']['href'])) { + return new RedirectResponse($this->getHost() . '/basket/finish?error=Mollie'); + } + + return new RedirectResponse($response['_links']['checkout']['href']); + } + + public function handleNotify(Request $request) + { + $paymentId = $request->request->get('id'); + + if (!$paymentId) { + return new Response('', 200); + } + + /** @var Mollie $settings */ + $settings = $this->getGatewaySettings()->getGatewayDocument(); + + $this->getPaymentService->setApiKey($settings->getActiveApiKey()); + $this->getPaymentService->setPaymentId($paymentId); + + try { + $payment = $this->getPaymentService->call(); + } catch (\Exception $e) { + return new Response('', 200); + } + + if (!isset($payment['status']) || $payment['status'] !== 'paid') { + return new Response('', 200); + } + + /** @var \PSC\Shop\EntityBundle\Document\Order $orderDoc */ + $orderDoc = $this->_doctrine_mongodb + ->getRepository('PSC\Shop\EntityBundle\Document\Order') + ->findOneBy(['paymentRef' => (string) $paymentId]); + + if (!$orderDoc) { + return new Response('', 200); + } + + /** @var \PSC\Shop\EntityBundle\Entity\Order $order */ + $order = $this->_entityManager + ->getRepository('PSC\Shop\EntityBundle\Entity\Order') + ->findOneBy(['uid' => $orderDoc->getUid()]); + + $order->setStatus(145); + $this->_entityManager->persist($order); + $this->_entityManager->flush(); + + $notify = new Payed(); + $notify->setShop($this->getShopEntity()->getUID()); + $notify->setOrder($order->getUuid()); + + $this->eventManager->addJob($notify); + + return new Response('', 200); + } + + public function doPayment(Request $request) {} + + /** + * Mollie verlangt eine öffentlich erreichbare Webhook-URL. + * Auf lokalen Hosts (localhost, *.local, *.test, private IPs) wird der + * Webhook deshalb übersprungen, damit lokale Tests möglich sind. + */ + private function isPubliclyReachable(string $host): bool + { + $parsed = parse_url($host, PHP_URL_HOST) ?: $host; + $parsed = strtolower($parsed); + + if ($parsed === 'localhost' || $parsed === '127.0.0.1' || $parsed === '::1') { + return false; + } + + if (str_ends_with($parsed, '.local') || str_ends_with($parsed, '.test') || str_ends_with($parsed, '.localhost')) { + return false; + } + + if (filter_var($parsed, FILTER_VALIDATE_IP)) { + return (bool) filter_var( + $parsed, + FILTER_VALIDATE_IP, + FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE, + ); + } + + return true; + } +} diff --git a/src/new/var/plugins/System/PSC/Mollie/Plugin.php b/src/new/var/plugins/System/PSC/Mollie/Plugin.php new file mode 100644 index 000000000..acc9abfc4 --- /dev/null +++ b/src/new/var/plugins/System/PSC/Mollie/Plugin.php @@ -0,0 +1,24 @@ + + +
+ {{ form_widget(form.mollie.production, {attr: {'class': 'form-control'}}) }} +
+ + +
+ +
+ {{ form_widget(form.mollie.apiKey, {attr: {'class': 'form-control'}}) }} +
+
+ +
+ +
+ {{ form_widget(form.mollie.testApiKey, {attr: {'class': 'form-control'}}) }} +
+
diff --git a/src/new/version.yaml b/src/new/version.yaml index 43e00a291..7c5afd02b 100755 --- a/src/new/version.yaml +++ b/src/new/version.yaml @@ -6,6 +6,7 @@ changelog: - version: 2.3.5 datum: 01.04.2026 changes: + - "Mollie Zahlungsanbieter" - "Aktionen der An und Von Name wird jetzt ans Mailsystem übergeben" - text: "CMS ContentBuilder Plugin kann aktiviert. (Custom Plugin und das Storefront Template muss die Ausgabe unterstützen)" images: