Files
fotospiel-app/app/Http/Controllers/PayPalController.php
Codex Agent a949c8d3af - Wired the checkout wizard for Google “comfort login”: added Socialite controller + dependency, new Google env
hooks in config/services.php/.env.example, and updated wizard steps/controllers to store session payloads,
attach packages, and surface localized success/error states.
- Retooled payment handling for both Stripe and PayPal, adding richer status management in CheckoutController/
PayPalController, fallback flows in the wizard’s PaymentStep.tsx, and fresh feature tests for intent
creation, webhooks, and the wizard CTA.
- Introduced a consent-aware Matomo analytics stack: new consent context, cookie-banner UI, useAnalytics/
useCtaExperiment hooks, and MatomoTracker component, then instrumented marketing pages (Home, Packages,
Checkout) with localized copy and experiment tracking.
- Polished package presentation across marketing UIs by centralizing formatting in PresentsPackages, surfacing
localized description tables/placeholders, tuning badges/layouts, and syncing guest/marketing translations.
- Expanded docs & reference material (docs/prp/*, TODOs, public gallery overview) and added a Playwright smoke
test for the hero CTA while reconciling outstanding checklist items.
2025-10-19 11:41:03 +02:00

265 lines
9.7 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;
use Illuminate\Support\Facades\Auth;
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([
'package_id' => 'required|exists:packages,id',
'tenant_id' => 'nullable|exists:tenants,id',
]);
$tenant = $request->tenant_id
? Tenant::findOrFail($request->tenant_id)
: optional(Auth::user())->tenant;
if (! $tenant) {
return response()->json(['error' => 'Tenant context required for checkout.'], 422);
}
$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([
'package_id' => 'required|exists:packages,id',
'plan_id' => 'required|string',
'tenant_id' => 'nullable|exists:tenants,id',
]);
$tenant = $request->tenant_id
? Tenant::findOrFail($request->tenant_id)
: optional(Auth::user())->tenant;
if (! $tenant) {
return response()->json(['error' => 'Tenant context required for subscription checkout.'], 422);
}
$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);
}
}
}