Add PayPal, Stripe, CSV-Export; fix .env.production issue
Deploy to Production (witstore.wende.it) / deploy (push) Successful in 7s
Deploy to Production (witstore.wende.it) / deploy (push) Successful in 7s
- PayPal: Checkout-Integration via srmklive/paypal, Weiterleitung zu PayPal, Bestellung wird nach Capture automatisch auf payment_received gesetzt - Stripe: Payment Elements im Checkout, Intent-API, Zahlung direkt bestätigt - CSV-Export: Admin → Bestellungen → CSV exportieren (UTF-8 BOM für Excel) - PayPal/Stripe nur sichtbar wenn Keys in .env gesetzt - .env.example um PAYPAL_* und STRIPE_* Keys erweitert Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -43,3 +43,12 @@ MAIL_FROM_ADDRESS="shop@example.de"
|
|||||||
MAIL_FROM_NAME="Mein Online-Shop"
|
MAIL_FROM_NAME="Mein Online-Shop"
|
||||||
|
|
||||||
VITE_APP_NAME="${APP_NAME}"
|
VITE_APP_NAME="${APP_NAME}"
|
||||||
|
|
||||||
|
PAYPAL_MODE=sandbox
|
||||||
|
PAYPAL_SANDBOX_CLIENT_ID=
|
||||||
|
PAYPAL_SANDBOX_CLIENT_SECRET=
|
||||||
|
PAYPAL_LIVE_CLIENT_ID=
|
||||||
|
PAYPAL_LIVE_CLIENT_SECRET=
|
||||||
|
|
||||||
|
STRIPE_KEY=
|
||||||
|
STRIPE_SECRET=
|
||||||
|
|||||||
@@ -76,4 +76,39 @@ class OrderController extends Controller
|
|||||||
{
|
{
|
||||||
return $this->invoiceService->generate($order)->download($this->invoiceService->filename($order));
|
return $this->invoiceService->generate($order)->download($this->invoiceService->filename($order));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function exportCsv()
|
||||||
|
{
|
||||||
|
$orders = Order::with(['items','billingAddress'])->orderByDesc('created_at')->get();
|
||||||
|
|
||||||
|
$rows = [];
|
||||||
|
$rows[] = ['Bestellnr.','Datum','Status','Zahlungsart','Kunde','E-Mail','Zwischensumme','Rabatt','Versand','Gesamt','MwSt.'];
|
||||||
|
|
||||||
|
foreach ($orders as $order) {
|
||||||
|
$addr = $order->billingAddress();
|
||||||
|
$rows[] = [
|
||||||
|
$order->order_number,
|
||||||
|
$order->created_at->format('d.m.Y H:i'),
|
||||||
|
Order::$statusLabels[$order->status] ?? $order->status,
|
||||||
|
Order::$paymentLabels[$order->payment_method] ?? $order->payment_method,
|
||||||
|
$addr ? $addr->full_name : ($order->guest_email ?? ''),
|
||||||
|
$order->guest_email ?? $order->user?->email ?? '',
|
||||||
|
number_format($order->subtotal, 2, ',', '.'),
|
||||||
|
number_format($order->discount_amount ?? 0, 2, ',', '.'),
|
||||||
|
number_format($order->shipping_cost, 2, ',', '.'),
|
||||||
|
number_format($order->total, 2, ',', '.'),
|
||||||
|
number_format($order->tax_total, 2, ',', '.'),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
$csv = "\xEF\xBB\xBF"; // UTF-8 BOM für Excel
|
||||||
|
foreach ($rows as $row) {
|
||||||
|
$csv .= implode(';', array_map(fn($v) => '"' . str_replace('"', '""', $v) . '"', $row)) . "\n";
|
||||||
|
}
|
||||||
|
|
||||||
|
return response($csv, 200, [
|
||||||
|
'Content-Type' => 'text/csv; charset=UTF-8',
|
||||||
|
'Content-Disposition' => 'attachment; filename="bestellungen-' . date('Y-m-d') . '.csv"',
|
||||||
|
]);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -103,6 +103,10 @@ class CheckoutController extends Controller
|
|||||||
|
|
||||||
session(['checkout_data' => $data]);
|
session(['checkout_data' => $data]);
|
||||||
|
|
||||||
|
if ($data['payment_method'] === 'paypal') {
|
||||||
|
return redirect()->route('paypal.redirect');
|
||||||
|
}
|
||||||
|
|
||||||
return redirect()->route('checkout.confirm');
|
return redirect()->route('checkout.confirm');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,104 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Controllers\Shop;
|
||||||
|
|
||||||
|
use App\Http\Controllers\Controller;
|
||||||
|
use App\Services\CartService;
|
||||||
|
use App\Services\OrderService;
|
||||||
|
use App\Models\Coupon;
|
||||||
|
use App\Models\Order;
|
||||||
|
use App\Models\ShippingRate;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
use Srmklive\PayPal\Services\PayPal as PayPalClient;
|
||||||
|
|
||||||
|
class PayPalController extends Controller
|
||||||
|
{
|
||||||
|
public function __construct(private CartService $cart, private OrderService $orderService) {}
|
||||||
|
|
||||||
|
public function redirect(Request $request)
|
||||||
|
{
|
||||||
|
$checkoutData = session('checkout_data');
|
||||||
|
if (!$checkoutData || $this->cart->count() === 0) {
|
||||||
|
return redirect()->route('checkout.index');
|
||||||
|
}
|
||||||
|
|
||||||
|
$subtotal = $this->cart->subtotal();
|
||||||
|
$country = $checkoutData['shipping']['country'] ?? $checkoutData['billing']['country'] ?? 'DE';
|
||||||
|
$shippingRate = ShippingRate::forCart($subtotal, $country);
|
||||||
|
$shippingCost = $shippingRate ? $shippingRate->getCostForTotal($subtotal) : 0;
|
||||||
|
$discount = 0;
|
||||||
|
if ($code = session('coupon_code')) {
|
||||||
|
$coupon = Coupon::findValid($code);
|
||||||
|
if ($coupon) $discount = $coupon->calcDiscount($subtotal);
|
||||||
|
}
|
||||||
|
$total = max(0, $subtotal - $discount) + $shippingCost;
|
||||||
|
|
||||||
|
$provider = new PayPalClient;
|
||||||
|
$provider->setApiCredentials(config('paypal'));
|
||||||
|
$provider->getAccessToken();
|
||||||
|
|
||||||
|
$order = $provider->createOrder([
|
||||||
|
'intent' => 'CAPTURE',
|
||||||
|
'purchase_units' => [[
|
||||||
|
'amount' => [
|
||||||
|
'currency_code' => 'EUR',
|
||||||
|
'value' => number_format($total, 2, '.', ''),
|
||||||
|
],
|
||||||
|
'description' => config('app.name') . ' Bestellung',
|
||||||
|
]],
|
||||||
|
'application_context' => [
|
||||||
|
'return_url' => route('paypal.success'),
|
||||||
|
'cancel_url' => route('checkout.confirm'),
|
||||||
|
],
|
||||||
|
]);
|
||||||
|
|
||||||
|
if (isset($order['id'])) {
|
||||||
|
session(['paypal_order_id' => $order['id']]);
|
||||||
|
foreach ($order['links'] as $link) {
|
||||||
|
if ($link['rel'] === 'approve') {
|
||||||
|
return redirect($link['href']);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return redirect()->route('checkout.confirm')->with('error', 'PayPal-Verbindung fehlgeschlagen. Bitte versuchen Sie es erneut.');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function success(Request $request)
|
||||||
|
{
|
||||||
|
$checkoutData = session('checkout_data');
|
||||||
|
$paypalOrderId = session('paypal_order_id');
|
||||||
|
|
||||||
|
if (!$checkoutData || !$paypalOrderId) {
|
||||||
|
return redirect()->route('checkout.index');
|
||||||
|
}
|
||||||
|
|
||||||
|
$provider = new PayPalClient;
|
||||||
|
$provider->setApiCredentials(config('paypal'));
|
||||||
|
$provider->getAccessToken();
|
||||||
|
|
||||||
|
$result = $provider->capturePaymentOrder($paypalOrderId);
|
||||||
|
|
||||||
|
if (($result['status'] ?? '') !== 'COMPLETED') {
|
||||||
|
return redirect()->route('checkout.confirm')->with('error', 'PayPal-Zahlung nicht abgeschlossen.');
|
||||||
|
}
|
||||||
|
|
||||||
|
$checkoutData['payment_method'] = 'paypal';
|
||||||
|
$order = $this->orderService->createFromSession($checkoutData, 'paypal');
|
||||||
|
$order->update([
|
||||||
|
'payment_id' => $paypalOrderId,
|
||||||
|
'status' => 'payment_received',
|
||||||
|
'paid_at' => now(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
session()->forget(['checkout_data', 'checkout_country', 'paypal_order_id']);
|
||||||
|
|
||||||
|
return redirect()->route('checkout.thankyou', $order->order_number);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function cancel()
|
||||||
|
{
|
||||||
|
session()->forget('paypal_order_id');
|
||||||
|
return redirect()->route('checkout.confirm')->with('error', 'PayPal-Zahlung abgebrochen.');
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,69 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Controllers\Shop;
|
||||||
|
|
||||||
|
use App\Http\Controllers\Controller;
|
||||||
|
use App\Models\Coupon;
|
||||||
|
use App\Models\Order;
|
||||||
|
use App\Models\ShippingRate;
|
||||||
|
use App\Services\CartService;
|
||||||
|
use App\Services\OrderService;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
use Stripe\StripeClient;
|
||||||
|
|
||||||
|
class StripeController extends Controller
|
||||||
|
{
|
||||||
|
public function __construct(private CartService $cart, private OrderService $orderService) {}
|
||||||
|
|
||||||
|
public function createIntent(Request $request)
|
||||||
|
{
|
||||||
|
$subtotal = $this->cart->subtotal();
|
||||||
|
$country = session('checkout_country', 'DE');
|
||||||
|
$shippingRate = ShippingRate::forCart($subtotal, $country);
|
||||||
|
$shippingCost = $shippingRate ? $shippingRate->getCostForTotal($subtotal) : 0;
|
||||||
|
$discount = 0;
|
||||||
|
if ($code = session('coupon_code')) {
|
||||||
|
$coupon = Coupon::findValid($code);
|
||||||
|
if ($coupon) $discount = $coupon->calcDiscount($subtotal);
|
||||||
|
}
|
||||||
|
$total = max(0, $subtotal - $discount) + $shippingCost;
|
||||||
|
|
||||||
|
$stripe = new StripeClient(config('services.stripe.secret'));
|
||||||
|
$intent = $stripe->paymentIntents->create([
|
||||||
|
'amount' => (int) round($total * 100),
|
||||||
|
'currency' => 'eur',
|
||||||
|
'automatic_payment_methods' => ['enabled' => true],
|
||||||
|
]);
|
||||||
|
|
||||||
|
return response()->json(['client_secret' => $intent->client_secret]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function success(Request $request)
|
||||||
|
{
|
||||||
|
$checkoutData = session('checkout_data');
|
||||||
|
$paymentIntent = $request->input('payment_intent');
|
||||||
|
|
||||||
|
if (!$checkoutData || !$paymentIntent) {
|
||||||
|
return redirect()->route('checkout.index');
|
||||||
|
}
|
||||||
|
|
||||||
|
$stripe = new StripeClient(config('services.stripe.secret'));
|
||||||
|
$intent = $stripe->paymentIntents->retrieve($paymentIntent);
|
||||||
|
|
||||||
|
if ($intent->status !== 'succeeded') {
|
||||||
|
return redirect()->route('checkout.confirm')->with('error', 'Zahlung nicht abgeschlossen.');
|
||||||
|
}
|
||||||
|
|
||||||
|
$checkoutData['payment_method'] = 'stripe';
|
||||||
|
$order = $this->orderService->createFromSession($checkoutData, 'stripe');
|
||||||
|
$order->update([
|
||||||
|
'payment_id' => $paymentIntent,
|
||||||
|
'status' => 'payment_received',
|
||||||
|
'paid_at' => now(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
session()->forget(['checkout_data', 'checkout_country']);
|
||||||
|
|
||||||
|
return redirect()->route('checkout.thankyou', $order->order_number);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -9,7 +9,7 @@ use Illuminate\Database\Eloquent\Relations\HasMany;
|
|||||||
class Order extends Model
|
class Order extends Model
|
||||||
{
|
{
|
||||||
protected $fillable = [
|
protected $fillable = [
|
||||||
'order_number','user_id','guest_email','status','payment_method',
|
'order_number','user_id','guest_email','status','payment_method','payment_id',
|
||||||
'coupon_code','discount_amount',
|
'coupon_code','discount_amount',
|
||||||
'subtotal','tax_total','shipping_cost','total','tracking_code','notes','paid_at',
|
'subtotal','tax_total','shipping_cost','total','tracking_code','notes','paid_at',
|
||||||
];
|
];
|
||||||
@@ -35,6 +35,8 @@ class Order extends Model
|
|||||||
public static array $paymentLabels = [
|
public static array $paymentLabels = [
|
||||||
'prepayment' => 'Vorkasse',
|
'prepayment' => 'Vorkasse',
|
||||||
'invoice' => 'Rechnung',
|
'invoice' => 'Rechnung',
|
||||||
|
'paypal' => 'PayPal',
|
||||||
|
'stripe' => 'Kreditkarte (Stripe)',
|
||||||
];
|
];
|
||||||
|
|
||||||
public function user(): BelongsTo
|
public function user(): BelongsTo
|
||||||
|
|||||||
+3
-1
@@ -10,7 +10,9 @@
|
|||||||
"barryvdh/laravel-dompdf": "^3.1",
|
"barryvdh/laravel-dompdf": "^3.1",
|
||||||
"laravel/breeze": "^2.4",
|
"laravel/breeze": "^2.4",
|
||||||
"laravel/framework": "^13.8",
|
"laravel/framework": "^13.8",
|
||||||
"laravel/tinker": "^3.0"
|
"laravel/tinker": "^3.0",
|
||||||
|
"srmklive/paypal": "^3.1",
|
||||||
|
"stripe/stripe-php": "^20.2"
|
||||||
},
|
},
|
||||||
"require-dev": {
|
"require-dev": {
|
||||||
"fakerphp/faker": "^1.23",
|
"fakerphp/faker": "^1.23",
|
||||||
|
|||||||
Generated
+127
-1
@@ -4,7 +4,7 @@
|
|||||||
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
|
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
|
||||||
"This file is @generated automatically"
|
"This file is @generated automatically"
|
||||||
],
|
],
|
||||||
"content-hash": "7c85fc2534f4d32bbb1b8b81e2f24574",
|
"content-hash": "21ae14678f4d08df3df9ad60a51f440e",
|
||||||
"packages": [
|
"packages": [
|
||||||
{
|
{
|
||||||
"name": "barryvdh/laravel-dompdf",
|
"name": "barryvdh/laravel-dompdf",
|
||||||
@@ -3744,6 +3744,132 @@
|
|||||||
},
|
},
|
||||||
"time": "2026-06-18T15:10:53+00:00"
|
"time": "2026-06-18T15:10:53+00:00"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"name": "srmklive/paypal",
|
||||||
|
"version": "3.1.1",
|
||||||
|
"source": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "https://github.com/blendbyte/laravel-paypal.git",
|
||||||
|
"reference": "522834a22fbd10e8fa1be21cb7c84d3d45c3a763"
|
||||||
|
},
|
||||||
|
"dist": {
|
||||||
|
"type": "zip",
|
||||||
|
"url": "https://api.github.com/repos/blendbyte/laravel-paypal/zipball/522834a22fbd10e8fa1be21cb7c84d3d45c3a763",
|
||||||
|
"reference": "522834a22fbd10e8fa1be21cb7c84d3d45c3a763",
|
||||||
|
"shasum": ""
|
||||||
|
},
|
||||||
|
"require": {
|
||||||
|
"guzzlehttp/guzzle": "^7.9",
|
||||||
|
"guzzlehttp/psr7": "^2.0",
|
||||||
|
"illuminate/support": "^12.0|^13.0",
|
||||||
|
"nesbot/carbon": "^3.0",
|
||||||
|
"php": "^8.2",
|
||||||
|
"psr/http-client": "^1.0"
|
||||||
|
},
|
||||||
|
"require-dev": {
|
||||||
|
"larastan/larastan": "^3.0",
|
||||||
|
"laravel/framework": "^12.0|^13.0",
|
||||||
|
"laravel/pint": "^1.29",
|
||||||
|
"orchestra/testbench": "^10.0|^11.0",
|
||||||
|
"pestphp/pest": "^3.0|^4.0",
|
||||||
|
"phpunit/phpunit": "^11.0|^12.0",
|
||||||
|
"symfony/var-dumper": "^7.0"
|
||||||
|
},
|
||||||
|
"type": "library",
|
||||||
|
"extra": {
|
||||||
|
"laravel": {
|
||||||
|
"aliases": {
|
||||||
|
"PayPal": "Srmklive\\PayPal\\Facades\\PayPal"
|
||||||
|
},
|
||||||
|
"providers": [
|
||||||
|
"Srmklive\\PayPal\\Providers\\PayPalServiceProvider"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"autoload": {
|
||||||
|
"psr-4": {
|
||||||
|
"Srmklive\\PayPal\\": "src/"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"notification-url": "https://packagist.org/downloads/",
|
||||||
|
"license": [
|
||||||
|
"MIT"
|
||||||
|
],
|
||||||
|
"description": "PayPal REST API client for Laravel and standalone PHP.",
|
||||||
|
"keywords": [
|
||||||
|
"REST API",
|
||||||
|
"laravel",
|
||||||
|
"payments",
|
||||||
|
"paypal"
|
||||||
|
],
|
||||||
|
"support": {
|
||||||
|
"issues": "https://github.com/blendbyte/laravel-paypal/issues",
|
||||||
|
"source": "https://github.com/blendbyte/laravel-paypal/tree/3.1.1"
|
||||||
|
},
|
||||||
|
"time": "2026-04-18T05:16:56+00:00"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "stripe/stripe-php",
|
||||||
|
"version": "v20.2.1",
|
||||||
|
"source": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "https://github.com/stripe/stripe-php.git",
|
||||||
|
"reference": "c628cfa0b3de4ef5110b2c2bfbf881a33a52fdd5"
|
||||||
|
},
|
||||||
|
"dist": {
|
||||||
|
"type": "zip",
|
||||||
|
"url": "https://api.github.com/repos/stripe/stripe-php/zipball/c628cfa0b3de4ef5110b2c2bfbf881a33a52fdd5",
|
||||||
|
"reference": "c628cfa0b3de4ef5110b2c2bfbf881a33a52fdd5",
|
||||||
|
"shasum": ""
|
||||||
|
},
|
||||||
|
"require": {
|
||||||
|
"ext-curl": "*",
|
||||||
|
"ext-json": "*",
|
||||||
|
"ext-mbstring": "*",
|
||||||
|
"php": ">=7.2.0"
|
||||||
|
},
|
||||||
|
"require-dev": {
|
||||||
|
"friendsofphp/php-cs-fixer": "3.94.0",
|
||||||
|
"phpstan/phpstan": "^1.2",
|
||||||
|
"phpunit/phpunit": "^8.0 || ^9.0"
|
||||||
|
},
|
||||||
|
"type": "library",
|
||||||
|
"extra": {
|
||||||
|
"branch-alias": {
|
||||||
|
"dev-master": "2.0-dev"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"autoload": {
|
||||||
|
"files": [
|
||||||
|
"lib/version_check.php"
|
||||||
|
],
|
||||||
|
"psr-4": {
|
||||||
|
"Stripe\\": "lib/"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"notification-url": "https://packagist.org/downloads/",
|
||||||
|
"license": [
|
||||||
|
"MIT"
|
||||||
|
],
|
||||||
|
"authors": [
|
||||||
|
{
|
||||||
|
"name": "Stripe and contributors",
|
||||||
|
"homepage": "https://github.com/stripe/stripe-php/contributors"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"description": "Stripe PHP Library",
|
||||||
|
"homepage": "https://stripe.com/",
|
||||||
|
"keywords": [
|
||||||
|
"api",
|
||||||
|
"payment processing",
|
||||||
|
"stripe"
|
||||||
|
],
|
||||||
|
"support": {
|
||||||
|
"issues": "https://github.com/stripe/stripe-php/issues",
|
||||||
|
"source": "https://github.com/stripe/stripe-php/tree/v20.2.1"
|
||||||
|
},
|
||||||
|
"time": "2026-06-12T22:41:56+00:00"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"name": "symfony/clock",
|
"name": "symfony/clock",
|
||||||
"version": "v8.1.0",
|
"version": "v8.1.0",
|
||||||
|
|||||||
@@ -0,0 +1,31 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
/**
|
||||||
|
* PayPal Setting & API Credentials
|
||||||
|
* Created by Raza Mehdi <srmk@outlook.com>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
return [
|
||||||
|
'mode' => env('PAYPAL_MODE', 'sandbox'), // Can only be 'sandbox' Or 'live'. If empty or invalid, 'live' will be used.
|
||||||
|
'sandbox' => [
|
||||||
|
'client_id' => env('PAYPAL_SANDBOX_CLIENT_ID', ''),
|
||||||
|
'client_secret' => env('PAYPAL_SANDBOX_CLIENT_SECRET', ''),
|
||||||
|
'app_id' => 'APP-80W284485P519543T', // Sandbox app_id is always this fixed value.
|
||||||
|
],
|
||||||
|
'live' => [
|
||||||
|
'client_id' => env('PAYPAL_LIVE_CLIENT_ID', ''),
|
||||||
|
'client_secret' => env('PAYPAL_LIVE_CLIENT_SECRET', ''),
|
||||||
|
// Live app_id: log in to developer.paypal.com → My Apps & Credentials →
|
||||||
|
// select your app → the App ID shown at the top (starts with "APP-").
|
||||||
|
'app_id' => env('PAYPAL_LIVE_APP_ID', ''),
|
||||||
|
],
|
||||||
|
|
||||||
|
'payment_action' => env('PAYPAL_PAYMENT_ACTION', 'Sale'), // Can only be 'Sale', 'Authorization' or 'Order'
|
||||||
|
'currency' => env('PAYPAL_CURRENCY', 'USD'),
|
||||||
|
'notify_url' => env('PAYPAL_NOTIFY_URL', ''), // Change this accordingly for your application.
|
||||||
|
'locale' => env('PAYPAL_LOCALE', 'en_US'), // force gateway language i.e. it_IT, es_ES, en_US ... (for express checkout only)
|
||||||
|
'validate_ssl' => env('PAYPAL_VALIDATE_SSL', true), // Validate SSL when creating api client.
|
||||||
|
'timeout' => env('PAYPAL_TIMEOUT', 30), // Total request timeout in seconds.
|
||||||
|
'connect_timeout' => env('PAYPAL_CONNECT_TIMEOUT', 10), // Connection timeout in seconds.
|
||||||
|
'max_retries' => env('PAYPAL_MAX_RETRIES', 2), // Retries on 5xx / connection errors (0 to disable). Uses exponential backoff.
|
||||||
|
];
|
||||||
@@ -28,6 +28,11 @@ return [
|
|||||||
'region' => env('AWS_DEFAULT_REGION', 'us-east-1'),
|
'region' => env('AWS_DEFAULT_REGION', 'us-east-1'),
|
||||||
],
|
],
|
||||||
|
|
||||||
|
'stripe' => [
|
||||||
|
'key' => env('STRIPE_KEY'),
|
||||||
|
'secret' => env('STRIPE_SECRET'),
|
||||||
|
],
|
||||||
|
|
||||||
'slack' => [
|
'slack' => [
|
||||||
'notifications' => [
|
'notifications' => [
|
||||||
'bot_user_oauth_token' => env('SLACK_BOT_USER_OAUTH_TOKEN'),
|
'bot_user_oauth_token' => env('SLACK_BOT_USER_OAUTH_TOKEN'),
|
||||||
|
|||||||
@@ -0,0 +1,25 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
|
||||||
|
return new class extends Migration {
|
||||||
|
public function up(): void
|
||||||
|
{
|
||||||
|
Schema::table('orders', function (Blueprint $table) {
|
||||||
|
$table->string('payment_id')->nullable()->after('payment_method');
|
||||||
|
});
|
||||||
|
|
||||||
|
// payment_method enum um paypal und stripe erweitern
|
||||||
|
DB::statement("ALTER TABLE orders MODIFY COLUMN payment_method ENUM('prepayment','invoice','paypal','stripe') NOT NULL DEFAULT 'prepayment'");
|
||||||
|
}
|
||||||
|
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
Schema::table('orders', function (Blueprint $table) {
|
||||||
|
$table->dropColumn('payment_id');
|
||||||
|
});
|
||||||
|
DB::statement("ALTER TABLE orders MODIFY COLUMN payment_method ENUM('prepayment','invoice') NOT NULL DEFAULT 'prepayment'");
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -1,9 +1,10 @@
|
|||||||
@extends('layouts.admin')
|
@extends('layouts.admin')
|
||||||
@section('title', 'Bestellungen')
|
@section('title', 'Bestellungen')
|
||||||
@section('content')
|
@section('content')
|
||||||
<div class="flex items-center gap-3 mb-6">
|
<div class="flex items-center gap-3 mb-6 flex-wrap">
|
||||||
<h2 class="text-lg font-semibold">Bestellungen</h2>
|
<h2 class="text-lg font-semibold">Bestellungen</h2>
|
||||||
<form method="GET" class="flex gap-2 ml-4">
|
<a href="{{ route('admin.orders.export') }}" class="ml-auto bg-green-600 text-white px-4 py-1.5 rounded hover:bg-green-700 text-sm shrink-0">↓ CSV exportieren</a>
|
||||||
|
<form method="GET" class="flex gap-2 w-full">
|
||||||
<input type="text" name="q" value="{{ request('q') }}" placeholder="Bestellnr." class="border rounded px-3 py-1.5 text-sm w-36">
|
<input type="text" name="q" value="{{ request('q') }}" placeholder="Bestellnr." class="border rounded px-3 py-1.5 text-sm w-36">
|
||||||
<select name="status" class="border rounded px-2 py-1.5 text-sm">
|
<select name="status" class="border rounded px-2 py-1.5 text-sm">
|
||||||
<option value="">Alle Status</option>
|
<option value="">Alle Status</option>
|
||||||
|
|||||||
@@ -1,5 +1,53 @@
|
|||||||
@extends('layouts.shop')
|
@extends('layouts.shop')
|
||||||
@section('title', 'Kasse')
|
@section('title', 'Kasse')
|
||||||
|
@push('scripts')
|
||||||
|
@if(config('services.stripe.key'))
|
||||||
|
<script src="https://js.stripe.com/v3/"></script>
|
||||||
|
<script>
|
||||||
|
const stripe = Stripe('{{ config('services.stripe.key') }}');
|
||||||
|
const elements = stripe.elements();
|
||||||
|
const cardElement = elements.create('card', { style: { base: { fontSize: '16px' } } });
|
||||||
|
|
||||||
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
|
cardElement.mount('#stripe-card-element');
|
||||||
|
cardElement.on('change', ({ error }) => {
|
||||||
|
const el = document.getElementById('stripe-errors');
|
||||||
|
if (error) { el.textContent = error.message; el.classList.remove('hidden'); }
|
||||||
|
else { el.classList.add('hidden'); }
|
||||||
|
});
|
||||||
|
|
||||||
|
const form = document.querySelector('form[action="{{ route('checkout.address') }}"]');
|
||||||
|
form.addEventListener('submit', async (e) => {
|
||||||
|
const method = document.querySelector('input[name="payment_method"]:checked')?.value;
|
||||||
|
if (method !== 'stripe') return;
|
||||||
|
e.preventDefault();
|
||||||
|
|
||||||
|
const res = await fetch('{{ route('stripe.intent') }}', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'X-CSRF-TOKEN': '{{ csrf_token() }}', 'Content-Type': 'application/json' },
|
||||||
|
});
|
||||||
|
const { client_secret } = await res.json();
|
||||||
|
|
||||||
|
const { error, paymentIntent } = await stripe.confirmCardPayment(client_secret, {
|
||||||
|
payment_method: { card: cardElement }
|
||||||
|
});
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
document.getElementById('stripe-errors').textContent = error.message;
|
||||||
|
document.getElementById('stripe-errors').classList.remove('hidden');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Adresse speichern und Stripe-Erfolg-URL aufrufen
|
||||||
|
const formData = new FormData(form);
|
||||||
|
formData.set('payment_method', 'stripe');
|
||||||
|
await fetch('{{ route('checkout.address') }}', { method: 'POST', body: formData });
|
||||||
|
window.location.href = '{{ route('stripe.success') }}?payment_intent=' + paymentIntent.id;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
@endif
|
||||||
|
@endpush
|
||||||
@section('content')
|
@section('content')
|
||||||
<h1 class="text-2xl font-bold mb-6">Kasse</h1>
|
<h1 class="text-2xl font-bold mb-6">Kasse</h1>
|
||||||
|
|
||||||
@@ -134,23 +182,58 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{{-- Zahlungsart --}}
|
{{-- Zahlungsart --}}
|
||||||
<div class="bg-white border rounded-lg p-6 mb-6">
|
<div class="bg-white border rounded-lg p-6 mb-6" x-data="{ method: 'prepayment' }">
|
||||||
<h2 class="font-semibold mb-4">Zahlungsart</h2>
|
<h2 class="font-semibold mb-4">Zahlungsart</h2>
|
||||||
<div class="space-y-3">
|
<div class="space-y-3">
|
||||||
<label class="flex items-start gap-3 p-3 border rounded-lg cursor-pointer hover:bg-gray-50">
|
<label class="flex items-start gap-3 p-3 border rounded-lg cursor-pointer hover:bg-gray-50"
|
||||||
<input type="radio" name="payment_method" value="prepayment" class="mt-1" checked>
|
:class="method==='prepayment' ? 'border-blue-500 bg-blue-50' : ''">
|
||||||
|
<input type="radio" name="payment_method" value="prepayment" class="mt-1"
|
||||||
|
x-model="method" @change="method='prepayment'">
|
||||||
<div>
|
<div>
|
||||||
<div class="font-medium">Vorkasse (Überweisung)</div>
|
<div class="font-medium">Vorkasse (Überweisung)</div>
|
||||||
<div class="text-sm text-gray-500">Sie überweisen den Betrag auf unser Bankkonto. Wir versenden nach Zahlungseingang.</div>
|
<div class="text-sm text-gray-500">Sie überweisen den Betrag auf unser Bankkonto. Wir versenden nach Zahlungseingang.</div>
|
||||||
</div>
|
</div>
|
||||||
</label>
|
</label>
|
||||||
<label class="flex items-start gap-3 p-3 border rounded-lg cursor-pointer hover:bg-gray-50">
|
<label class="flex items-start gap-3 p-3 border rounded-lg cursor-pointer hover:bg-gray-50"
|
||||||
<input type="radio" name="payment_method" value="invoice" class="mt-1">
|
:class="method==='invoice' ? 'border-blue-500 bg-blue-50' : ''">
|
||||||
|
<input type="radio" name="payment_method" value="invoice" class="mt-1"
|
||||||
|
x-model="method" @change="method='invoice'">
|
||||||
<div>
|
<div>
|
||||||
<div class="font-medium">Kauf auf Rechnung</div>
|
<div class="font-medium">Kauf auf Rechnung</div>
|
||||||
<div class="text-sm text-gray-500">Sie erhalten die Rechnung per E-Mail. Zahlungsziel: {{ \App\Models\Setting::get('invoice_payment_days', 14) }} Tage.</div>
|
<div class="text-sm text-gray-500">Sie erhalten die Rechnung per E-Mail. Zahlungsziel: {{ \App\Models\Setting::get('invoice_payment_days', 14) }} Tage.</div>
|
||||||
</div>
|
</div>
|
||||||
</label>
|
</label>
|
||||||
|
@if(config('paypal.live.client_id') || config('paypal.sandbox.client_id'))
|
||||||
|
<label class="flex items-start gap-3 p-3 border rounded-lg cursor-pointer hover:bg-gray-50"
|
||||||
|
:class="method==='paypal' ? 'border-blue-500 bg-blue-50' : ''">
|
||||||
|
<input type="radio" name="payment_method" value="paypal" class="mt-1"
|
||||||
|
x-model="method" @change="method='paypal'">
|
||||||
|
<div>
|
||||||
|
<div class="font-medium flex items-center gap-2">
|
||||||
|
<svg viewBox="0 0 24 24" class="h-5" fill="none"><rect width="24" height="24" rx="4" fill="#003087"/><path d="M8.5 17H6l2-10h4c1.7 0 2.8.9 2.5 2.5-.4 2-2 3-3.8 3H9.2L8.5 17z" fill="#fff"/><path d="M10.5 11.5H12c1.3 0 2.2-.6 2.5-2 .2-1-.3-1.5-1.3-1.5h-1.5l-1.2 3.5z" fill="#009cde"/></svg>
|
||||||
|
PayPal
|
||||||
|
</div>
|
||||||
|
<div class="text-sm text-gray-500">Sofort und sicher mit PayPal bezahlen.</div>
|
||||||
|
</div>
|
||||||
|
</label>
|
||||||
|
@endif
|
||||||
|
@if(config('services.stripe.key'))
|
||||||
|
<label class="flex items-start gap-3 p-3 border rounded-lg cursor-pointer hover:bg-gray-50"
|
||||||
|
:class="method==='stripe' ? 'border-blue-500 bg-blue-50' : ''">
|
||||||
|
<input type="radio" name="payment_method" value="stripe" class="mt-1"
|
||||||
|
x-model="method" @change="method='stripe'">
|
||||||
|
<div>
|
||||||
|
<div class="font-medium">Kreditkarte (Stripe)</div>
|
||||||
|
<div class="text-sm text-gray-500">Visa, Mastercard, American Express — verschlüsselt über Stripe.</div>
|
||||||
|
</div>
|
||||||
|
</label>
|
||||||
|
@if(config('services.stripe.key'))
|
||||||
|
<div x-show="method==='stripe'" class="mt-3 p-3 border rounded-lg bg-gray-50">
|
||||||
|
<div id="stripe-card-element" class="p-3 bg-white border rounded"></div>
|
||||||
|
<p id="stripe-errors" class="text-red-500 text-sm mt-2 hidden"></p>
|
||||||
|
</div>
|
||||||
|
@endif
|
||||||
|
@endif
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -174,9 +257,20 @@
|
|||||||
</div>
|
</div>
|
||||||
@endif
|
@endif
|
||||||
|
|
||||||
<button type="submit" class="w-full bg-blue-600 text-white py-3 rounded-lg hover:bg-blue-700 font-semibold text-lg">
|
<button type="submit" x-show="method!=='paypal'"
|
||||||
Bestellung prüfen →
|
class="w-full btn-primary py-3 rounded-lg font-semibold text-lg">
|
||||||
|
<span x-text="method==='stripe' ? 'Jetzt bezahlen (Kreditkarte)' : 'Bestellung prüfen →'"></span>
|
||||||
</button>
|
</button>
|
||||||
|
{{-- PayPal: Adresse speichern, dann zu PayPal weiterleiten --}}
|
||||||
|
<div x-show="method==='paypal'" class="space-y-2">
|
||||||
|
<input type="hidden" name="payment_method" value="paypal" x-bind:value="method">
|
||||||
|
<button type="submit" formaction="{{ route('checkout.address') }}"
|
||||||
|
onclick="this.form.action='{{ route('checkout.address') }}'"
|
||||||
|
class="w-full bg-yellow-400 hover:bg-yellow-500 text-gray-900 py-3 rounded-lg font-semibold text-lg flex items-center justify-center gap-2">
|
||||||
|
<svg viewBox="0 0 24 24" class="h-6 w-6"><rect width="24" height="24" rx="4" fill="#003087"/><path d="M8.5 17H6l2-10h4c1.7 0 2.8.9 2.5 2.5-.4 2-2 3-3.8 3H9.2L8.5 17z" fill="#fff"/></svg>
|
||||||
|
Mit PayPal bezahlen
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -25,6 +25,15 @@ Route::post('/kasse/adresse', [Shop\CheckoutController::class, 'storeAddress'])-
|
|||||||
Route::get('/kasse/zahlung', [Shop\CheckoutController::class, 'payment'])->name('checkout.payment');
|
Route::get('/kasse/zahlung', [Shop\CheckoutController::class, 'payment'])->name('checkout.payment');
|
||||||
Route::match(['GET','POST'], '/kasse/bestaetigen', [Shop\CheckoutController::class, 'confirm'])->name('checkout.confirm');
|
Route::match(['GET','POST'], '/kasse/bestaetigen', [Shop\CheckoutController::class, 'confirm'])->name('checkout.confirm');
|
||||||
Route::get('/kasse/danke/{order}', [Shop\CheckoutController::class, 'thankyou'])->name('checkout.thankyou');
|
Route::get('/kasse/danke/{order}', [Shop\CheckoutController::class, 'thankyou'])->name('checkout.thankyou');
|
||||||
|
// PayPal
|
||||||
|
Route::get('/kasse/paypal/weiterleitung', [Shop\PayPalController::class, 'redirect'])->name('paypal.redirect');
|
||||||
|
Route::get('/kasse/paypal/erfolg', [Shop\PayPalController::class, 'success'])->name('paypal.success');
|
||||||
|
Route::get('/kasse/paypal/abbruch', [Shop\PayPalController::class, 'cancel'])->name('paypal.cancel');
|
||||||
|
|
||||||
|
// Stripe
|
||||||
|
Route::post('/kasse/stripe/intent', [Shop\StripeController::class, 'createIntent'])->name('stripe.intent');
|
||||||
|
Route::get('/kasse/stripe/erfolg', [Shop\StripeController::class, 'success'])->name('stripe.success');
|
||||||
|
|
||||||
Route::post('/kasse/gutschein', [Shop\CheckoutController::class, 'applyCoupon'])->name('checkout.coupon.apply');
|
Route::post('/kasse/gutschein', [Shop\CheckoutController::class, 'applyCoupon'])->name('checkout.coupon.apply');
|
||||||
Route::post('/kasse/gutschein/entfernen', [Shop\CheckoutController::class, 'removeCoupon'])->name('checkout.coupon.remove');
|
Route::post('/kasse/gutschein/entfernen', [Shop\CheckoutController::class, 'removeCoupon'])->name('checkout.coupon.remove');
|
||||||
Route::get('/kasse/versandkosten', [Shop\CheckoutController::class, 'shippingCost'])->name('checkout.shipping.cost');
|
Route::get('/kasse/versandkosten', [Shop\CheckoutController::class, 'shippingCost'])->name('checkout.shipping.cost');
|
||||||
@@ -86,6 +95,7 @@ Route::middleware(['auth', 'admin'])->prefix('admin')->name('admin.')->group(fun
|
|||||||
|
|
||||||
// Bestellungen
|
// Bestellungen
|
||||||
Route::get('orders', [Admin\OrderController::class, 'index'])->name('orders.index');
|
Route::get('orders', [Admin\OrderController::class, 'index'])->name('orders.index');
|
||||||
|
Route::get('orders/export/csv', [Admin\OrderController::class, 'exportCsv'])->name('orders.export');
|
||||||
Route::get('orders/{order}', [Admin\OrderController::class, 'show'])->name('orders.show');
|
Route::get('orders/{order}', [Admin\OrderController::class, 'show'])->name('orders.show');
|
||||||
Route::put('orders/{order}/status', [Admin\OrderController::class, 'updateStatus'])->name('orders.status');
|
Route::put('orders/{order}/status', [Admin\OrderController::class, 'updateStatus'])->name('orders.status');
|
||||||
Route::post('orders/{order}/invoice', [Admin\OrderController::class, 'sendInvoice'])->name('orders.invoice');
|
Route::post('orders/{order}/invoice', [Admin\OrderController::class, 'sendInvoice'])->name('orders.invoice');
|
||||||
|
|||||||
Reference in New Issue
Block a user