This commit is contained in:
Thomas Peterson 2026-04-15 17:00:54 +02:00
parent 332e8982ec
commit 2632d08946
9 changed files with 505 additions and 0 deletions

View File

@ -0,0 +1,62 @@
<?php
namespace Plugin\System\PSC\Mollie\Api;
use Symfony\Contracts\HttpClient\HttpClientInterface;
class Base
{
protected string $baseUrl = 'https://api.mollie.com';
protected \PSC\Shop\EntityBundle\Document\Shop $shop;
/**
* @var HttpClientInterface
*/
protected HttpClientInterface $client;
protected string $apiKey = '';
public function __construct(HttpClientInterface $client)
{
$this->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;
}
}

View File

@ -0,0 +1,84 @@
<?php
namespace Plugin\System\PSC\Mollie\Api;
use PSC\Shop\EntityBundle\Entity\ContactAddress;
class CreatePayment extends Base
{
protected ?ContactAddress $invoiceAddress = null;
protected string $description = '';
protected string $redirectUrl = '';
protected string $webhookUrl = '';
protected ?string $orderReference = null;
public function call(): array
{
return $this->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;
}
}

View File

@ -0,0 +1,18 @@
<?php
namespace Plugin\System\PSC\Mollie\Api;
class GetPayment extends Base
{
protected string $paymentId = '';
public function call(): array
{
return $this->getV2('/v2/payments/' . urlencode($this->paymentId));
}
public function setPaymentId(string $paymentId): void
{
$this->paymentId = $paymentId;
}
}

View File

@ -0,0 +1,65 @@
<?php
namespace Plugin\System\PSC\Mollie\Document;
use Doctrine\ODM\MongoDB\Mapping\Annotations\EmbeddedDocument;
use Doctrine\ODM\MongoDB\Mapping\Annotations\Field;
#[EmbeddedDocument]
class Mollie
{
/**
* @var string $apiKey
*/
#[Field(type: 'string')]
protected $apiKey;
/**
* @var string $testApiKey
*/
#[Field(type: 'string')]
protected $testApiKey;
/**
* @var boolean $production
*/
#[Field(type: 'bool')]
protected $production;
public function getApiKey(): string
{
return (string)$this->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();
}
}

View File

@ -0,0 +1,219 @@
<?php
namespace Plugin\System\PSC\Mollie\Payment;
use Doctrine\ODM\MongoDB\DocumentManager;
use Doctrine\ORM\EntityManagerInterface;
use Plugin\System\PSC\Mollie\Api\CreatePayment;
use Plugin\System\PSC\Mollie\Api\GetPayment;
use Plugin\System\PSC\Mollie\Document\Mollie;
use PSC\Shop\EntityBundle\Entity\Contact;
use PSC\Shop\EntityBundle\Entity\ContactAddress;
use PSC\Shop\PaymentBundle\Document\Gatewaysettings;
use PSC\Shop\PaymentBundle\Provider\PaymentProvider;
use PSC\Shop\QueueBundle\Event\Order\Payed;
use PSC\Shop\QueueBundle\Service\Event\Manager;
use Symfony\Component\Form\Extension\Core\Type\CheckboxType;
use Symfony\Component\Form\Extension\Core\Type\TextType;
use Symfony\Component\Form\Form;
use Symfony\Component\Form\FormBuilder;
use Symfony\Component\Form\FormFactoryInterface;
use Symfony\Component\HttpFoundation\RedirectResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
class Provider extends PaymentProvider
{
/** @var Form */
private $_formFactory = null;
private $_entityManager = null;
private $_doctrine_mongodb = null;
private \PSC\System\SettingsBundle\Service\Shop $shopService;
private Manager $eventManager;
private CreatePayment $createPaymentService;
private GetPayment $getPaymentService;
public function __construct(
CreatePayment $createPayment,
GetPayment $getPayment,
Manager $eventManager,
FormFactoryInterface $formFactory,
EntityManagerInterface $entityManager,
DocumentManager $doctrine_mongodb,
\PSC\System\SettingsBundle\Service\Shop $shopService,
) {
$this->_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;
}
}

View File

@ -0,0 +1,24 @@
<?php
namespace Plugin\System\PSC\Mollie;
use PSC\System\PluginBundle\Plugin\Base;
class Plugin extends Base implements \PSC\System\PluginBundle\Interfaces\Plugin {
protected $name = 'Mollie Payment';
public function getType()
{
return Plugin::Payment;
}
public function getDescription()
{
return 'Stellt Unterstützung für Mollie bereit';
}
public function getVersion()
{
return 1;
}
}

View File

@ -0,0 +1,11 @@
services:
_defaults:
autowire: true
autoconfigure: true
Plugin\System\PSC\Mollie\:
resource: '../../*/*'
Plugin\System\PSC\Mollie\Payment\Provider:
tags:
- { name: paymentProvider }

View File

@ -0,0 +1,21 @@
<div class="form-group row">
<label class="col-md-4 form-control-label">{{ form_label(form.mollie.production) }}</label>
<div class="col-md-8">
{{ form_widget(form.mollie.production, {attr: {'class': 'form-control'}}) }}
</div>
</div>
<div class="form-group row">
<label class="col-md-4 form-control-label">{{ form_label(form.mollie.apiKey) }}</label>
<div class="col-md-8">
{{ form_widget(form.mollie.apiKey, {attr: {'class': 'form-control'}}) }}
</div>
</div>
<div class="form-group row">
<label class="col-md-4 form-control-label">{{ form_label(form.mollie.testApiKey) }}</label>
<div class="col-md-8">
{{ form_widget(form.mollie.testApiKey, {attr: {'class': 'form-control'}}) }}
</div>
</div>

View File

@ -6,6 +6,7 @@ changelog:
- version: 2.3.5 - version: 2.3.5
datum: 01.04.2026 datum: 01.04.2026
changes: changes:
- "Mollie Zahlungsanbieter"
- "Aktionen der An und Von Name wird jetzt ans Mailsystem übergeben" - "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)" - text: "CMS ContentBuilder Plugin kann aktiviert. (Custom Plugin und das Storefront Template muss die Ausgabe unterstützen)"
images: images: