Files
fotospiel-app/app/Http/Controllers/PayPalController.php
Codex Agent d04e234ca0 - Tenant-Admin-PWA: Neues /event-admin/welcome Onboarding mit WelcomeHero, Packages-, Order-Summary- und Event-Setup-Pages, Zustandsspeicher, Routing-Guard und Dashboard-CTA für Erstnutzer; Filament-/admin-Login via Custom-View behoben.
- Brand/Theming: Marketing-Farb- und Typographievariablen in `resources/css/app.css` eingeführt, AdminLayout, Dashboardkarten und Onboarding-Komponenten entsprechend angepasst; Dokumentation (`docs/todo/tenant-admin-onboarding-fusion.md`, `docs/changes/...`) aktualisiert.
- Checkout & Payments: Checkout-, PayPal-Controller und Tests für integrierte Stripe/PayPal-Flows sowie Paket-Billing-Abläufe überarbeitet; neue PayPal SDK-Factory und Admin-API-Helper (`resources/js/admin/api.ts`) schaffen Grundlage für Billing/Members/Tasks-Seiten.
- DX & Tests: Neue Playwright/E2E-Struktur (docs/testing/e2e.md, `tests/e2e/tenant-onboarding-flow.test.ts`, Utilities), E2E-Tenant-Seeder und zusätzliche Übersetzungen/Factories zur Unterstützung der neuen Flows.
- Marketing-Kommunikation: Automatische Kontakt-Bestätigungsmail (`ContactConfirmation` + Blade-Template) implementiert; Guest-PWA unter `/event` erreichbar.
- Nebensitzung: Blogsystem gefixt und umfassenden BlogPostSeeder für Beispielinhalte angelegt.
2025-10-10 21:31:55 +02:00

251 lines
9.2 KiB
PHP

<?php
namespace App\Http\Controllers;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Log;
use App\Models\PackagePurchase;
use App\Models\Tenant;
use App\Models\TenantPackage;
use App\Models\Package;
use PaypalServerSdkLib\Models\Builders\OrderRequestBuilder;
use PaypalServerSdkLib\Models\Builders\PurchaseUnitRequestBuilder;
use PaypalServerSdkLib\Models\Builders\AmountWithBreakdownBuilder;
use PaypalServerSdkLib\Models\Builders\OrderApplicationContextBuilder;
use PaypalServerSdkLib\Models\CheckoutPaymentIntent;
use App\Services\PayPal\PaypalClientFactory;
class PayPalController extends Controller
{
private $client;
private PaypalClientFactory $clientFactory;
public function __construct(PaypalClientFactory $clientFactory)
{
$this->clientFactory = $clientFactory;
$this->client = $clientFactory->make();
}
public function createOrder(Request $request)
{
$request->validate([
'tenant_id' => 'required|exists:tenants,id',
'package_id' => 'required|exists:packages,id',
]);
$tenant = Tenant::findOrFail($request->tenant_id);
$package = Package::findOrFail($request->package_id);
$ordersController = $this->client->getOrdersController();
$body = OrderRequestBuilder::init(
CheckoutPaymentIntent::CAPTURE,
[
PurchaseUnitRequestBuilder::init(
AmountWithBreakdownBuilder::init('EUR', number_format($package->price, 2, '.', ''))
->build()
)
->description('Package: ' . $package->name)
->customId(json_encode([
'tenant_id' => $tenant->id,
'package_id' => $package->id,
'type' => 'endcustomer_event'
]))
->build()
]
)
->applicationContext(
OrderApplicationContextBuilder::init()
->brandName('Fotospiel')
->landingPage('BILLING')
->build()
)
->build();
$collect = [
'body' => $body,
'prefer' => 'return=representation'
];
try {
$response = $ordersController->createOrder($collect);
if ($response->getStatusCode() === 201) {
$result = $response->getResult();
$approveLink = collect($result->links)->first(fn($link) => $link->rel === 'approve')?->href;
return response()->json([
'id' => $result->id,
'approve_url' => $approveLink,
]);
}
Log::error('PayPal order creation failed', ['response' => $response]);
return response()->json(['error' => 'Order creation failed'], 400);
} catch (\Exception $e) {
Log::error('PayPal order creation exception', ['error' => $e->getMessage()]);
return response()->json(['error' => 'Order creation failed'], 500);
}
}
public function captureOrder(Request $request)
{
$request->validate(['order_id' => 'required']);
$ordersController = $this->client->getOrdersController();
$collect = [
'id' => $request->order_id,
'prefer' => 'return=representation'
];
try {
$response = $ordersController->captureOrder($collect);
if ($response->getStatusCode() === 201) {
$result = $response->getResult();
$customId = $result->purchaseUnits[0]->customId ?? null;
if ($customId) {
$metadata = json_decode($customId, true);
$tenantId = $metadata['tenant_id'] ?? null;
$packageId = $metadata['package_id'] ?? null;
$type = $metadata['type'] ?? 'endcustomer_event';
if ($tenantId && $packageId) {
$tenant = Tenant::findOrFail($tenantId);
$package = Package::findOrFail($packageId);
PackagePurchase::create([
'tenant_id' => $tenant->id,
'package_id' => $package->id,
'provider_id' => $result->id,
'price' => $result->purchaseUnits[0]->amount->value,
'type' => $type,
'purchased_at' => now(),
'refunded' => false,
]);
TenantPackage::create([
'tenant_id' => $tenant->id,
'package_id' => $package->id,
'price' => $package->price,
'purchased_at' => now(),
'active' => true,
]);
$tenant->update(['subscription_status' => 'active']);
} else {
Log::error('Invalid metadata in PayPal custom_id', ['custom_id' => $customId]);
}
Log::info('PayPal order captured and purchase created: ' . $result->id);
}
return response()->json(['status' => 'captured', 'order' => $result]);
}
Log::error('PayPal order capture failed', ['response' => $response]);
return response()->json(['error' => 'Capture failed'], 400);
} catch (\Exception $e) {
Log::error('PayPal order capture exception', ['error' => $e->getMessage()]);
return response()->json(['error' => 'Capture failed'], 500);
}
}
public function createSubscription(Request $request)
{
$request->validate([
'tenant_id' => 'required|exists:tenants,id',
'package_id' => 'required|exists:packages,id',
'plan_id' => 'required', // PayPal plan ID for the package
]);
$tenant = Tenant::findOrFail($request->tenant_id);
$package = Package::findOrFail($request->package_id);
$ordersController = $this->client->getOrdersController();
$storedPaymentSource = new \PaypalServerSdkLib\Models\StoredPaymentSource(
'CUSTOMER',
'RECURRING'
);
$storedPaymentSource->setUsage('FIRST');
$paymentSource = new \PaypalServerSdkLib\Models\PaymentSource();
$paymentSource->storedPaymentSource = $storedPaymentSource;
$body = OrderRequestBuilder::init(
CheckoutPaymentIntent::CAPTURE,
[
PurchaseUnitRequestBuilder::init(
AmountWithBreakdownBuilder::init('EUR', number_format($package->price, 2, '.', ''))
->build()
)
->description('Subscription Package: ' . $package->name)
->customId(json_encode([
'tenant_id' => $tenant->id,
'package_id' => $package->id,
'type' => 'reseller_subscription',
'plan_id' => $request->plan_id
]))
->build()
]
)
->paymentSource($paymentSource)
->applicationContext(
OrderApplicationContextBuilder::init()
->brandName('Fotospiel')
->landingPage('BILLING')
->build()
)
->build();
$collect = [
'body' => $body,
'prefer' => 'return=representation'
];
try {
$response = $ordersController->createOrder($collect);
if ($response->getStatusCode() === 201) {
$result = $response->getResult();
$orderId = $result->id;
// Initial purchase record for subscription setup
TenantPackage::create([
'tenant_id' => $tenant->id,
'package_id' => $package->id,
'price' => $package->price,
'purchased_at' => now(),
'expires_at' => now()->addYear(), // Assuming annual subscription
'active' => true,
]);
PackagePurchase::create([
'tenant_id' => $tenant->id,
'package_id' => $package->id,
'provider_id' => $orderId . '_sub_' . $request->plan_id, // Combine for uniqueness
'price' => $package->price,
'type' => 'reseller_subscription',
'purchased_at' => now(),
]);
$approveLink = collect($result->links)->first(fn($link) => $link->rel === 'approve')?->href;
return response()->json([
'order_id' => $orderId,
'approve_url' => $approveLink,
]);
}
Log::error('PayPal subscription order creation failed', ['response' => $response]);
return response()->json(['error' => 'Subscription order creation failed'], 400);
} catch (\Exception $e) {
Log::error('PayPal subscription order creation exception', ['error' => $e->getMessage()]);
return response()->json(['error' => 'Subscription order creation failed'], 500);
}
}
}