Newwebshop/app/Core/Payment.php

662 lines
22 KiB
PHP

<?php
/**
* Copyright seit 2024 Webshop System
*
* Payment Core-Klasse für das Webshop-System
*
* @author Webshop System
* @license GPL v3
*/
namespace App\Core;
use Doctrine\DBAL\DriverManager;
use Doctrine\DBAL\Exception;
class Payment
{
private $conn;
private $config;
private $multiShop;
// Payment Provider Konfiguration
private $providers = [
'paypal' => [
'name' => 'PayPal',
'enabled' => false,
'sandbox' => true,
'client_id' => '',
'client_secret' => '',
'webhook_id' => ''
],
'stripe' => [
'name' => 'Stripe',
'enabled' => false,
'test_mode' => true,
'publishable_key' => '',
'secret_key' => '',
'webhook_secret' => ''
],
'sepa' => [
'name' => 'SEPA-Lastschrift',
'enabled' => false,
'test_mode' => true,
'merchant_id' => '',
'iban' => '',
'bic' => ''
]
];
public function __construct()
{
$this->conn = DriverManager::getConnection([
'url' => getenv('DATABASE_URL') ?: 'mysql://root:password@localhost/webshop'
]);
$this->config = new Configuration();
$this->multiShop = new MultiShop();
$this->loadPaymentConfig();
}
/**
* Payment-Konfiguration laden
*/
private function loadPaymentConfig()
{
$shopId = $this->multiShop->getCurrentShopId();
foreach ($this->providers as $provider => &$config) {
$config['enabled'] = $this->multiShop->getShopConfig("PAYMENT_{$provider}_ENABLED", false);
switch ($provider) {
case 'paypal':
$config['sandbox'] = $this->multiShop->getShopConfig('PAYMENT_PAYPAL_SANDBOX', true);
$config['client_id'] = $this->multiShop->getShopConfig('PAYMENT_PAYPAL_CLIENT_ID', '');
$config['client_secret'] = $this->multiShop->getShopConfig('PAYMENT_PAYPAL_CLIENT_SECRET', '');
$config['webhook_id'] = $this->multiShop->getShopConfig('PAYMENT_PAYPAL_WEBHOOK_ID', '');
break;
case 'stripe':
$config['test_mode'] = $this->multiShop->getShopConfig('PAYMENT_STRIPE_TEST_MODE', true);
$config['publishable_key'] = $this->multiShop->getShopConfig('PAYMENT_STRIPE_PUBLISHABLE_KEY', '');
$config['secret_key'] = $this->multiShop->getShopConfig('PAYMENT_STRIPE_SECRET_KEY', '');
$config['webhook_secret'] = $this->multiShop->getShopConfig('PAYMENT_STRIPE_WEBHOOK_SECRET', '');
break;
case 'sepa':
$config['test_mode'] = $this->multiShop->getShopConfig('PAYMENT_SEPA_TEST_MODE', true);
$config['merchant_id'] = $this->multiShop->getShopConfig('PAYMENT_SEPA_MERCHANT_ID', '');
$config['iban'] = $this->multiShop->getShopConfig('PAYMENT_SEPA_IBAN', '');
$config['bic'] = $this->multiShop->getShopConfig('PAYMENT_SEPA_BIC', '');
break;
}
}
}
/**
* Verfügbare Zahlungsmethoden abrufen
*/
public function getAvailablePaymentMethods()
{
$methods = [];
foreach ($this->providers as $provider => $config) {
if ($config['enabled']) {
$methods[$provider] = [
'name' => $config['name'],
'provider' => $provider,
'enabled' => true,
'test_mode' => $config['sandbox'] ?? $config['test_mode'] ?? false
];
}
}
return $methods;
}
/**
* PayPal Payment erstellen
*/
public function createPayPalPayment($orderData)
{
if (!$this->providers['paypal']['enabled']) {
throw new \Exception('PayPal ist nicht aktiviert');
}
$paypalConfig = $this->providers['paypal'];
$apiUrl = $paypalConfig['sandbox'] ? 'https://api-m.sandbox.paypal.com' : 'https://api-m.paypal.com';
// PayPal Access Token abrufen
$accessToken = $this->getPayPalAccessToken($apiUrl, $paypalConfig);
// PayPal Order erstellen
$paypalOrder = $this->createPayPalOrder($apiUrl, $accessToken, $orderData);
// Payment Transaction speichern
$transactionId = $this->savePaymentTransaction([
'order_id' => $orderData['order_id'],
'provider' => 'paypal',
'transaction_id' => $paypalOrder['id'],
'amount' => $orderData['total_amount'],
'currency' => $orderData['currency'],
'status' => 'pending',
'payment_data' => json_encode($paypalOrder)
]);
return [
'transaction_id' => $transactionId,
'paypal_order_id' => $paypalOrder['id'],
'approval_url' => $paypalOrder['links'][1]['href'] ?? null
];
}
/**
* PayPal Access Token abrufen
*/
private function getPayPalAccessToken($apiUrl, $config)
{
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $apiUrl . '/v1/oauth2/token');
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, 'grant_type=client_credentials');
curl_setopt($ch, CURLOPT_USERPWD, $config['client_id'] . ':' . $config['client_secret']);
curl_setopt($ch, CURLOPT_HTTPHEADER, [
'Content-Type: application/x-www-form-urlencoded'
]);
$response = curl_exec($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
if ($httpCode !== 200) {
throw new \Exception('PayPal Access Token Fehler: ' . $response);
}
$data = json_decode($response, true);
return $data['access_token'];
}
/**
* PayPal Order erstellen
*/
private function createPayPalOrder($apiUrl, $accessToken, $orderData)
{
$payload = [
'intent' => 'CAPTURE',
'purchase_units' => [
[
'reference_id' => $orderData['order_id'],
'amount' => [
'currency_code' => $orderData['currency'],
'value' => number_format($orderData['total_amount'], 2, '.', '')
],
'description' => 'Bestellung #' . $orderData['order_id']
]
],
'application_context' => [
'return_url' => getenv('BASE_URL') . '/payment/paypal/return',
'cancel_url' => getenv('BASE_URL') . '/payment/paypal/cancel'
]
];
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $apiUrl . '/v2/checkout/orders');
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($payload));
curl_setopt($ch, CURLOPT_HTTPHEADER, [
'Content-Type: application/json',
'Authorization: Bearer ' . $accessToken
]);
$response = curl_exec($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
if ($httpCode !== 201) {
throw new \Exception('PayPal Order Fehler: ' . $response);
}
return json_decode($response, true);
}
/**
* PayPal Payment verarbeiten
*/
public function processPayPalPayment($paypalOrderId)
{
$paypalConfig = $this->providers['paypal'];
$apiUrl = $paypalConfig['sandbox'] ? 'https://api-m.sandbox.paypal.com' : 'https://api-m.paypal.com';
$accessToken = $this->getPayPalAccessToken($apiUrl, $paypalConfig);
// PayPal Order erfassen
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $apiUrl . "/v2/checkout/orders/{$paypalOrderId}/capture");
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, [
'Content-Type: application/json',
'Authorization: Bearer ' . $accessToken
]);
$response = curl_exec($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
if ($httpCode !== 201) {
throw new \Exception('PayPal Capture Fehler: ' . $response);
}
$captureData = json_decode($response, true);
// Payment Transaction aktualisieren
$this->updatePaymentTransaction($paypalOrderId, [
'status' => 'completed',
'capture_id' => $captureData['purchase_units'][0]['payments']['captures'][0]['id'],
'payment_data' => json_encode($captureData)
]);
return $captureData;
}
/**
* Stripe Payment Intent erstellen
*/
public function createStripePaymentIntent($orderData)
{
if (!$this->providers['stripe']['enabled']) {
throw new \Exception('Stripe ist nicht aktiviert');
}
$stripeConfig = $this->providers['stripe'];
$apiUrl = $stripeConfig['test_mode'] ? 'https://api.stripe.com' : 'https://api.stripe.com';
$payload = [
'amount' => intval($orderData['total_amount'] * 100), // Stripe erwartet Cents
'currency' => strtolower($orderData['currency']),
'metadata' => [
'order_id' => $orderData['order_id']
],
'automatic_payment_methods' => [
'enabled' => true
]
];
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $apiUrl . '/v1/payment_intents');
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query($payload));
curl_setopt($ch, CURLOPT_HTTPHEADER, [
'Content-Type: application/x-www-form-urlencoded',
'Authorization: Bearer ' . $stripeConfig['secret_key']
]);
$response = curl_exec($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
if ($httpCode !== 200) {
throw new \Exception('Stripe Payment Intent Fehler: ' . $response);
}
$paymentIntent = json_decode($response, true);
// Payment Transaction speichern
$transactionId = $this->savePaymentTransaction([
'order_id' => $orderData['order_id'],
'provider' => 'stripe',
'transaction_id' => $paymentIntent['id'],
'amount' => $orderData['total_amount'],
'currency' => $orderData['currency'],
'status' => 'pending',
'payment_data' => json_encode($paymentIntent)
]);
return [
'transaction_id' => $transactionId,
'client_secret' => $paymentIntent['client_secret'],
'payment_intent_id' => $paymentIntent['id']
];
}
/**
* Stripe Webhook verarbeiten
*/
public function processStripeWebhook($payload, $signature)
{
$stripeConfig = $this->providers['stripe'];
// Webhook-Signatur verifizieren
$expectedSignature = hash_hmac('sha256', $payload, $stripeConfig['webhook_secret']);
if (!hash_equals($expectedSignature, $signature)) {
throw new \Exception('Stripe Webhook Signatur ungültig');
}
$event = json_decode($payload, true);
switch ($event['type']) {
case 'payment_intent.succeeded':
$this->handleStripePaymentSuccess($event['data']['object']);
break;
case 'payment_intent.payment_failed':
$this->handleStripePaymentFailure($event['data']['object']);
break;
}
return $event;
}
/**
* Stripe Payment Success Handler
*/
private function handleStripePaymentSuccess($paymentIntent)
{
$this->updatePaymentTransaction($paymentIntent['id'], [
'status' => 'completed',
'payment_data' => json_encode($paymentIntent)
]);
// Order Status aktualisieren
$orderId = $paymentIntent['metadata']['order_id'] ?? null;
if ($orderId) {
$this->updateOrderStatus($orderId, 'paid');
}
}
/**
* Stripe Payment Failure Handler
*/
private function handleStripePaymentFailure($paymentIntent)
{
$this->updatePaymentTransaction($paymentIntent['id'], [
'status' => 'failed',
'payment_data' => json_encode($paymentIntent)
]);
// Order Status aktualisieren
$orderId = $paymentIntent['metadata']['order_id'] ?? null;
if ($orderId) {
$this->updateOrderStatus($orderId, 'payment_failed');
}
}
/**
* SEPA-Lastschrift erstellen
*/
public function createSEPAPayment($orderData, $customerData)
{
if (!$this->providers['sepa']['enabled']) {
throw new \Exception('SEPA-Lastschrift ist nicht aktiviert');
}
$sepaConfig = $this->providers['sepa'];
// SEPA-Mandat erstellen
$mandateId = $this->createSEPAMandate($orderData['order_id'], $customerData);
// SEPA-Transaction erstellen
$sepaTransaction = [
'mandate_id' => $mandateId,
'amount' => $orderData['total_amount'],
'currency' => $orderData['currency'],
'debtor_iban' => $customerData['iban'],
'debtor_bic' => $customerData['bic'],
'debtor_name' => $customerData['name'],
'creditor_iban' => $sepaConfig['iban'],
'creditor_bic' => $sepaConfig['bic'],
'creditor_name' => $sepaConfig['merchant_id'],
'purpose' => 'Bestellung #' . $orderData['order_id']
];
// Payment Transaction speichern
$transactionId = $this->savePaymentTransaction([
'order_id' => $orderData['order_id'],
'provider' => 'sepa',
'transaction_id' => $mandateId,
'amount' => $orderData['total_amount'],
'currency' => $orderData['currency'],
'status' => 'pending',
'payment_data' => json_encode($sepaTransaction)
]);
return [
'transaction_id' => $transactionId,
'mandate_id' => $mandateId,
'status' => 'pending'
];
}
/**
* SEPA-Mandat erstellen
*/
private function createSEPAMandate($orderId, $customerData)
{
$mandateId = 'MNDT' . date('Ymd') . strtoupper(substr(md5($orderId), 0, 8));
try {
$stmt = $this->conn->prepare('
INSERT INTO ws_sepa_mandate (mandate_id, order_id, customer_id, iban, bic, name, created_at)
VALUES (?, ?, ?, ?, ?, ?, NOW())
');
$stmt->execute([
$mandateId,
$orderId,
$customerData['customer_id'],
$customerData['iban'],
$customerData['bic'],
$customerData['name']
]);
return $mandateId;
} catch (Exception $e) {
throw new \Exception('SEPA-Mandat Fehler: ' . $e->getMessage());
}
}
/**
* Payment Transaction speichern
*/
private function savePaymentTransaction($data)
{
try {
$stmt = $this->conn->prepare('
INSERT INTO ws_payment_transaction (
order_id, provider, transaction_id, amount, currency,
status, payment_data, created_at
) VALUES (?, ?, ?, ?, ?, ?, ?, NOW())
');
$stmt->execute([
$data['order_id'],
$data['provider'],
$data['transaction_id'],
$data['amount'],
$data['currency'],
$data['status'],
$data['payment_data']
]);
return $this->conn->lastInsertId();
} catch (Exception $e) {
throw new \Exception('Payment Transaction Fehler: ' . $e->getMessage());
}
}
/**
* Payment Transaction aktualisieren
*/
private function updatePaymentTransaction($transactionId, $data)
{
try {
$stmt = $this->conn->prepare('
UPDATE ws_payment_transaction
SET status = ?, payment_data = ?, updated_at = NOW()
WHERE transaction_id = ?
');
$stmt->execute([
$data['status'],
$data['payment_data'],
$transactionId
]);
} catch (Exception $e) {
throw new \Exception('Payment Transaction Update Fehler: ' . $e->getMessage());
}
}
/**
* Order Status aktualisieren
*/
private function updateOrderStatus($orderId, $status)
{
try {
$stmt = $this->conn->prepare('
UPDATE ws_order
SET status = ?, updated_at = NOW()
WHERE id = ?
');
$stmt->execute([$status, $orderId]);
} catch (Exception $e) {
error_log('Order Status Update Fehler: ' . $e->getMessage());
}
}
/**
* Payment-Statistiken abrufen
*/
public function getPaymentStatistics($shopId = null)
{
$shopId = $shopId ?: $this->multiShop->getCurrentShopId();
try {
$stmt = $this->conn->prepare('
SELECT
provider,
COUNT(*) as total_transactions,
SUM(amount) as total_amount,
COUNT(CASE WHEN status = "completed" THEN 1 END) as successful_transactions,
COUNT(CASE WHEN status = "failed" THEN 1 END) as failed_transactions,
AVG(amount) as average_amount
FROM ws_payment_transaction pt
JOIN ws_order o ON pt.order_id = o.id
WHERE o.shop_id = ?
GROUP BY provider
');
$stmt->execute([$shopId]);
return $stmt->fetchAllAssociative();
} catch (Exception $e) {
error_log('Payment Statistics Fehler: ' . $e->getMessage());
return [];
}
}
/**
* Payment-Konfiguration aktualisieren
*/
public function updatePaymentConfig($provider, $config)
{
$shopId = $this->multiShop->getCurrentShopId();
foreach ($config as $key => $value) {
$configKey = "PAYMENT_{$provider}_" . strtoupper($key);
$this->multiShop->setShopConfig($configKey, $value);
}
// Konfiguration neu laden
$this->loadPaymentConfig();
}
/**
* Payment-Provider Status prüfen
*/
public function checkPaymentProviderStatus($provider)
{
if (!isset($this->providers[$provider])) {
return false;
}
$config = $this->providers[$provider];
if (!$config['enabled']) {
return false;
}
// Provider-spezifische Tests
switch ($provider) {
case 'paypal':
return $this->testPayPalConnection();
case 'stripe':
return $this->testStripeConnection();
case 'sepa':
return $this->testSEPAConfiguration();
default:
return false;
}
}
/**
* PayPal-Verbindung testen
*/
private function testPayPalConnection()
{
try {
$config = $this->providers['paypal'];
$apiUrl = $config['sandbox'] ? 'https://api-m.sandbox.paypal.com' : 'https://api-m.paypal.com';
$accessToken = $this->getPayPalAccessToken($apiUrl, $config);
return !empty($accessToken);
} catch (\Exception $e) {
return false;
}
}
/**
* Stripe-Verbindung testen
*/
private function testStripeConnection()
{
try {
$config = $this->providers['stripe'];
$apiUrl = 'https://api.stripe.com';
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $apiUrl . '/v1/account');
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, [
'Authorization: Bearer ' . $config['secret_key']
]);
$response = curl_exec($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
return $httpCode === 200;
} catch (\Exception $e) {
return false;
}
}
/**
* SEPA-Konfiguration testen
*/
private function testSEPAConfiguration()
{
$config = $this->providers['sepa'];
return !empty($config['merchant_id']) &&
!empty($config['iban']) &&
!empty($config['bic']);
}
}