übergang auf pakete, integration von stripe und paypal, blog hinzugefügt.
This commit is contained in:
@@ -53,11 +53,7 @@ class EventController extends Controller
|
||||
$tenant = Tenant::findOrFail($tenantId);
|
||||
}
|
||||
|
||||
if (!$tenant->canCreateEvent()) {
|
||||
return response()->json([
|
||||
'error' => 'No available package for creating events. Please purchase a package.',
|
||||
], 402);
|
||||
}
|
||||
// Package check is now handled by middleware
|
||||
|
||||
$validated = $request->validated();
|
||||
$tenantId = $tenant->id;
|
||||
|
||||
94
app/Http/Controllers/Auth/MarketingRegisterController.php
Normal file
94
app/Http/Controllers/Auth/MarketingRegisterController.php
Normal file
@@ -0,0 +1,94 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Auth;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\Package;
|
||||
use App\Models\TenantPackage;
|
||||
use App\Models\User;
|
||||
use App\Models\Tenant;
|
||||
use Illuminate\Auth\Events\Registered;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Illuminate\Support\Facades\Hash;
|
||||
use Illuminate\Validation\Rules;
|
||||
use Illuminate\Support\Str;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Illuminate\View\View;
|
||||
|
||||
class MarketingRegisterController extends Controller
|
||||
{
|
||||
/**
|
||||
* Show the registration form.
|
||||
*/
|
||||
public function create(Request $request): View
|
||||
{
|
||||
$package = null;
|
||||
if ($request->has('package_id')) {
|
||||
$package = Package::findOrFail($request->package_id);
|
||||
}
|
||||
|
||||
return view('marketing.register', compact('package'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle an incoming registration request.
|
||||
*
|
||||
* @throws \Illuminate\Validation\ValidationException
|
||||
*/
|
||||
public function store(Request $request): RedirectResponse
|
||||
{
|
||||
$request->validate([
|
||||
'name' => ['required', 'string', 'max:255'],
|
||||
'username' => ['required', 'string', 'max:255', 'unique:' . User::class, 'alpha_dash'],
|
||||
'email' => ['required', 'string', 'email', 'max:255', 'unique:' . User::class],
|
||||
'password' => ['required', 'confirmed', Rules\Password::defaults()],
|
||||
'first_name' => ['required', 'string', 'max:255'],
|
||||
'last_name' => ['required', 'string', 'max:255'],
|
||||
'address' => ['required', 'string'],
|
||||
'phone' => ['required', 'string', 'max:20'],
|
||||
'privacy_consent' => ['required', 'accepted'],
|
||||
'package_id' => ['nullable', 'exists:packages,id'],
|
||||
]);
|
||||
|
||||
$user = User::create([
|
||||
'name' => $request->name,
|
||||
'username' => $request->username,
|
||||
'email' => $request->email,
|
||||
'password' => Hash::make($request->password),
|
||||
'first_name' => $request->first_name,
|
||||
'last_name' => $request->last_name,
|
||||
'address' => $request->address,
|
||||
'phone' => $request->phone,
|
||||
'preferred_locale' => $request->preferred_locale ?? 'de',
|
||||
]);
|
||||
|
||||
$tenant = Tenant::create([
|
||||
'user_id' => $user->id,
|
||||
'name' => $request->name,
|
||||
'slug' => Str::slug($request->name . '-' . now()->timestamp),
|
||||
'email' => $request->email,
|
||||
]);
|
||||
|
||||
// If package_id provided and free, assign immediately
|
||||
if ($request->package_id) {
|
||||
$package = Package::find($request->package_id);
|
||||
if ($package && $package->price == 0) {
|
||||
TenantPackage::create([
|
||||
'tenant_id' => $tenant->id,
|
||||
'package_id' => $package->id,
|
||||
'active' => true,
|
||||
'expires_at' => now()->addYear(), // or based on package duration
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
event(new Registered($user));
|
||||
|
||||
Auth::login($user);
|
||||
|
||||
return $user->hasVerifiedEmail()
|
||||
? redirect()->intended('/admin')
|
||||
: redirect()->route('verification.notice');
|
||||
}
|
||||
}
|
||||
@@ -12,6 +12,8 @@ use Illuminate\Support\Facades\Hash;
|
||||
use Illuminate\Validation\Rules;
|
||||
use Inertia\Inertia;
|
||||
use Inertia\Response;
|
||||
use App\Models\Tenant;
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
class RegisteredUserController extends Controller
|
||||
{
|
||||
@@ -42,6 +44,13 @@ class RegisteredUserController extends Controller
|
||||
'password' => Hash::make($request->password),
|
||||
]);
|
||||
|
||||
$tenant = Tenant::create([
|
||||
'user_id' => $user->id,
|
||||
'name' => $request->name,
|
||||
'slug' => Str::slug($request->name . '-' . now()->timestamp),
|
||||
'email' => $request->email,
|
||||
]);
|
||||
|
||||
event(new Registered($user));
|
||||
|
||||
Auth::login($user);
|
||||
|
||||
@@ -4,6 +4,7 @@ namespace App\Http\Controllers;
|
||||
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Mail;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
use Illuminate\Support\Str;
|
||||
use Stripe\Stripe;
|
||||
use Stripe\Checkout\Session;
|
||||
@@ -18,12 +19,16 @@ use PayPal\Rest\ApiContext;
|
||||
use PayPal\Auth\OAuthTokenCredential;
|
||||
use App\Models\Tenant;
|
||||
use App\Models\EventPurchase;
|
||||
use App\Models\Package;
|
||||
use App\Models\TenantPackage;
|
||||
use App\Models\PackagePurchase;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
|
||||
class MarketingController extends Controller
|
||||
{
|
||||
public function __construct()
|
||||
{
|
||||
\Stripe\Stripe::setApiKey(config('services.stripe.key'));
|
||||
Stripe::setApiKey(config('services.stripe.key'));
|
||||
}
|
||||
|
||||
public function index()
|
||||
@@ -53,74 +58,155 @@ class MarketingController extends Controller
|
||||
return redirect()->back()->with('success', 'Nachricht gesendet!');
|
||||
}
|
||||
|
||||
public function checkout(Request $request, $package)
|
||||
/**
|
||||
* Handle package purchase flow.
|
||||
*/
|
||||
public function buyPackages(Request $request, $packageId)
|
||||
{
|
||||
$packages = [
|
||||
'basic' => ['name' => 'Basic', 'price' => 0, 'events' => 1],
|
||||
'standard' => ['name' => 'Standard', 'price' => 9900, 'events' => 10], // cents
|
||||
'premium' => ['name' => 'Premium', 'price' => 19900, 'events' => 50],
|
||||
];
|
||||
$package = Package::findOrFail($packageId);
|
||||
|
||||
if (!isset($packages[$package])) {
|
||||
abort(404);
|
||||
if (!Auth::check()) {
|
||||
return redirect()->route('register', ['package_id' => $package->id])
|
||||
->with('message', __('marketing.packages.register_required'));
|
||||
}
|
||||
|
||||
$pkg = $packages[$package];
|
||||
$user = Auth::user();
|
||||
if (!$user->email_verified_at) {
|
||||
return redirect()->route('verification.notice')
|
||||
->with('message', __('auth.verification_required'));
|
||||
}
|
||||
|
||||
if ($pkg['price'] == 0) {
|
||||
// Free package: create tenant and event
|
||||
$tenant = Tenant::create([
|
||||
'name' => $request->input('tenant_name', 'New Tenant'),
|
||||
'slug' => Str::slug('new-' . now()),
|
||||
'email' => $request->input('email'),
|
||||
'events_remaining' => $pkg['events'],
|
||||
]);
|
||||
$tenant = $user->tenant;
|
||||
if (!$tenant) {
|
||||
abort(500, 'Tenant not found');
|
||||
}
|
||||
|
||||
// Create initial event
|
||||
$event = $tenant->events()->create([
|
||||
'name' => $request->input('event_name', 'My Event'),
|
||||
'slug' => Str::slug($request->input('event_name', 'my-event')),
|
||||
'status' => 'active',
|
||||
]);
|
||||
if ($package->price == 0) {
|
||||
TenantPackage::updateOrCreate(
|
||||
[
|
||||
'tenant_id' => $tenant->id,
|
||||
'package_id' => $package->id,
|
||||
],
|
||||
[
|
||||
'active' => true,
|
||||
'purchased_at' => now(),
|
||||
'expires_at' => now()->addYear(),
|
||||
]
|
||||
);
|
||||
|
||||
$purchase = EventPurchase::create([
|
||||
PackagePurchase::create([
|
||||
'tenant_id' => $tenant->id,
|
||||
'events_purchased' => $pkg['events'],
|
||||
'amount' => 0,
|
||||
'currency' => 'EUR',
|
||||
'provider' => 'free',
|
||||
'status' => 'completed',
|
||||
'package_id' => $package->id,
|
||||
'provider_id' => 'free',
|
||||
'price' => 0,
|
||||
'type' => $package->type,
|
||||
'purchased_at' => now(),
|
||||
'refunded' => false,
|
||||
]);
|
||||
|
||||
return redirect("/admin/tenants/{$tenant->id}/edit")->with('success', 'Konto erstellt! Willkommen bei Fotospiel.');
|
||||
return redirect('/admin')->with('success', __('marketing.packages.free_assigned'));
|
||||
}
|
||||
|
||||
$stripe = new \Stripe\StripeClient(config('services.stripe.secret'));
|
||||
if ($request->input('provider') === 'paypal') {
|
||||
return $this->paypalCheckout($request, $packageId);
|
||||
}
|
||||
|
||||
return $this->checkout($request, $packageId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checkout for Stripe with auth metadata.
|
||||
*/
|
||||
public function checkout(Request $request, $packageId)
|
||||
{
|
||||
$package = Package::findOrFail($packageId);
|
||||
$user = Auth::user();
|
||||
$tenant = $user->tenant;
|
||||
|
||||
$stripe = new StripeClient(config('services.stripe.secret'));
|
||||
$session = $stripe->checkout->sessions->create([
|
||||
'payment_method_types' => ['card'],
|
||||
'line_items' => [[
|
||||
'price_data' => [
|
||||
'currency' => 'eur',
|
||||
'product_data' => [
|
||||
'name' => $pkg['name'] . ' Package',
|
||||
'name' => $package->name,
|
||||
],
|
||||
'unit_amount' => $pkg['price'],
|
||||
'unit_amount' => $package->price * 100,
|
||||
],
|
||||
'quantity' => 1,
|
||||
]],
|
||||
'mode' => 'payment',
|
||||
'success_url' => route('marketing.success', $package),
|
||||
'cancel_url' => route('marketing'),
|
||||
'success_url' => route('marketing.success', $packageId),
|
||||
'cancel_url' => route('packages'),
|
||||
'metadata' => [
|
||||
'package' => $package,
|
||||
'events' => $pkg['events'],
|
||||
'user_id' => $user->id,
|
||||
'tenant_id' => $tenant->id,
|
||||
'package_id' => $package->id,
|
||||
'type' => $package->type,
|
||||
],
|
||||
]);
|
||||
|
||||
return redirect($session->url, 303);
|
||||
}
|
||||
|
||||
/**
|
||||
* PayPal checkout with auth metadata.
|
||||
*/
|
||||
public function paypalCheckout(Request $request, $packageId)
|
||||
{
|
||||
$package = Package::findOrFail($packageId);
|
||||
$user = Auth::user();
|
||||
$tenant = $user->tenant;
|
||||
|
||||
$apiContext = new ApiContext(
|
||||
new OAuthTokenCredential(
|
||||
config('services.paypal.client_id'),
|
||||
config('services.paypal.secret')
|
||||
)
|
||||
);
|
||||
|
||||
$payment = new Payment();
|
||||
$payer = new Payer();
|
||||
$payer->setPaymentMethod('paypal');
|
||||
|
||||
$amountObj = new Amount();
|
||||
$amountObj->setCurrency('EUR');
|
||||
$amountObj->setTotal($package->price);
|
||||
|
||||
$transaction = new Transaction();
|
||||
$transaction->setAmount($amountObj);
|
||||
|
||||
$redirectUrls = new RedirectUrls();
|
||||
$redirectUrls->setReturnUrl(route('marketing.success', $packageId));
|
||||
$redirectUrls->setCancelUrl(route('packages'));
|
||||
|
||||
$customData = json_encode([
|
||||
'user_id' => $user->id,
|
||||
'tenant_id' => $tenant->id,
|
||||
'package_id' => $package->id,
|
||||
'type' => $package->type,
|
||||
]);
|
||||
|
||||
$payment->setIntent('sale')
|
||||
->setPayer($payer)
|
||||
->setTransactions([$transaction])
|
||||
->setRedirectUrls($redirectUrls)
|
||||
->setNoteToPayer('Package: ' . $package->name)
|
||||
->setCustom($customData);
|
||||
|
||||
try {
|
||||
$payment->create($apiContext);
|
||||
|
||||
session(['paypal_payment_id' => $payment->getId()]);
|
||||
|
||||
return redirect($payment->getApprovalLink());
|
||||
} catch (\Exception $e) {
|
||||
Log::error('PayPal checkout error: ' . $e->getMessage());
|
||||
return back()->with('error', 'Zahlung fehlgeschlagen');
|
||||
}
|
||||
}
|
||||
|
||||
public function stripeCheckout($sessionId)
|
||||
{
|
||||
// Handle Stripe success
|
||||
@@ -154,82 +240,4 @@ class MarketingController extends Controller
|
||||
|
||||
return view('marketing.blog-show', compact('post'));
|
||||
}
|
||||
|
||||
public function paypalCheckout(Request $request, $package)
|
||||
{
|
||||
$packages = [
|
||||
'basic' => ['name' => 'Basic', 'price' => 0, 'events' => 1],
|
||||
'standard' => ['name' => 'Standard', 'price' => 99, 'events' => 10],
|
||||
'premium' => ['name' => 'Premium', 'price' => 199, 'events' => 50],
|
||||
];
|
||||
|
||||
if (!isset($packages[$package])) {
|
||||
abort(404);
|
||||
}
|
||||
|
||||
$pkg = $packages[$package];
|
||||
|
||||
if ($pkg['price'] == 0) {
|
||||
// Free package: create tenant and event
|
||||
$tenant = Tenant::create([
|
||||
'name' => $request->input('tenant_name', 'New Tenant'),
|
||||
'slug' => Str::slug('new-' . now()),
|
||||
'email' => $request->input('email'),
|
||||
'events_remaining' => $pkg['events'],
|
||||
]);
|
||||
|
||||
// Create initial event
|
||||
$event = $tenant->events()->create([
|
||||
'name' => $request->input('event_name', 'My Event'),
|
||||
'slug' => Str::slug($request->input('event_name', 'my-event')),
|
||||
'status' => 'active',
|
||||
]);
|
||||
|
||||
$purchase = EventPurchase::create([
|
||||
'tenant_id' => $tenant->id,
|
||||
'events_purchased' => $pkg['events'],
|
||||
'amount' => 0,
|
||||
'currency' => 'EUR',
|
||||
'provider' => 'free',
|
||||
'status' => 'completed',
|
||||
'purchased_at' => now(),
|
||||
]);
|
||||
|
||||
return redirect("/admin/tenants/{$tenant->id}/edit")->with('success', 'Konto erstellt! Willkommen bei Fotospiel.');
|
||||
}
|
||||
|
||||
$apiContext = new ApiContext(
|
||||
new OAuthTokenCredential(
|
||||
config('services.paypal.client_id'),
|
||||
config('services.paypal.secret')
|
||||
)
|
||||
);
|
||||
|
||||
$payment = new Payment();
|
||||
$payer = new Payer();
|
||||
$payer->setPaymentMethod('paypal');
|
||||
|
||||
$amountObj = new Amount();
|
||||
$amountObj->setCurrency('EUR');
|
||||
$amountObj->setTotal($pkg['price']);
|
||||
|
||||
$transaction = new Transaction();
|
||||
$transaction->setAmount($amountObj);
|
||||
|
||||
$redirectUrls = new RedirectUrls();
|
||||
$redirectUrls->setReturnUrl(route('marketing.success', $package));
|
||||
$redirectUrls->setCancelUrl(route('marketing'));
|
||||
|
||||
$payment->setIntent('sale')
|
||||
->setPayer($payer)
|
||||
->setTransactions([$transaction])
|
||||
->setRedirectUrls($redirectUrls);
|
||||
|
||||
try {
|
||||
$payment->create($apiContext);
|
||||
return redirect($payment->getApprovalLink());
|
||||
} catch (Exception $e) {
|
||||
return back()->with('error', 'Zahlung fehlgeschlagen');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
235
app/Http/Controllers/PayPalController.php
Normal file
235
app/Http/Controllers/PayPalController.php
Normal file
@@ -0,0 +1,235 @@
|
||||
<?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\PaypalServerSdkClientBuilder;
|
||||
use PaypalServerSdkLib\Auth\ClientCredentialsAuthCredentialsBuilder;
|
||||
use PaypalServerSdkLib\Environment;
|
||||
use PaypalServerSdkLib\Logging\LoggingConfigurationBuilder;
|
||||
use PaypalServerSdkLib\Logging\RequestLoggingConfigurationBuilder;
|
||||
use PaypalServerSdkLib\Logging\ResponseLoggingConfigurationBuilder;
|
||||
use PaypalServerSdkLib\Logging\LogLevel;
|
||||
use PaypalServerSdkLib\Orders\OrderRequestBuilder;
|
||||
use PaypalServerSdkLib\Orders\CheckoutPaymentIntent;
|
||||
use PaypalServerSdkLib\Orders\PurchaseUnitRequestBuilder;
|
||||
use PaypalServerSdkLib\Orders\AmountWithBreakdownBuilder;
|
||||
use PaypalServerSdkLib\Orders\ApplicationContextBuilder;
|
||||
use PaypalServerSdkLib\Subscriptions\SubscriptionRequestBuilder;
|
||||
use PaypalServerSdkLib\Subscriptions\SubscriberBuilder;
|
||||
use PaypalServerSdkLib\Subscriptions\NameBuilder;
|
||||
use PaypalServerSdkLib\Subscriptions\ApplicationContextSubscriptionBuilder;
|
||||
use PaypalServerSdkLib\Subscriptions\ShippingPreference;
|
||||
|
||||
class PayPalController extends Controller
|
||||
{
|
||||
private $client;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$clientId = config('services.paypal.client_id');
|
||||
$clientSecret = config('services.paypal.secret');
|
||||
|
||||
$this->client = PaypalServerSdkClientBuilder::init()
|
||||
->clientCredentialsAuthCredentials(
|
||||
ClientCredentialsAuthCredentialsBuilder::init($clientId, $clientSecret)
|
||||
)
|
||||
->environment(config('app.env') === 'production' ? Environment::PRODUCTION : Environment::SANDBOX)
|
||||
->loggingConfiguration(
|
||||
LoggingConfigurationBuilder::init()
|
||||
->level(LogLevel::INFO)
|
||||
->requestConfiguration(RequestLoggingConfigurationBuilder::init()->body(true))
|
||||
->responseConfiguration(ResponseLoggingConfigurationBuilder::init()->headers(true))
|
||||
)
|
||||
->build();
|
||||
}
|
||||
|
||||
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();
|
||||
|
||||
$requestBody = OrderRequestBuilder::init(CheckoutPaymentIntent::CAPTURE)
|
||||
->purchaseUnits([
|
||||
PurchaseUnitRequestBuilder::init()
|
||||
->amount(
|
||||
AmountWithBreakdownBuilder::init('EUR', number_format($package->price, 2, '.', ''))
|
||||
->build()
|
||||
)
|
||||
->description('Package: ' . $package->name)
|
||||
->customId($tenant->id . '_' . $package->id . '_endcustomer_event')
|
||||
->build()
|
||||
])
|
||||
->applicationContext(
|
||||
ApplicationContextBuilder::init()
|
||||
->shippingPreference(ShippingPreference::NO_SHIPPING)
|
||||
->userAction('PAY_NOW')
|
||||
->build()
|
||||
)
|
||||
->build();
|
||||
|
||||
$collect = [
|
||||
'body' => $requestBody,
|
||||
'prefer' => 'return=representation'
|
||||
];
|
||||
|
||||
try {
|
||||
$response = $ordersController->createOrder($collect);
|
||||
|
||||
if ($response->statusCode === 201) {
|
||||
$result = $response->result;
|
||||
$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->statusCode === 201) {
|
||||
$result = $response->result;
|
||||
$customId = $result->purchaseUnits[0]->customId ?? null;
|
||||
|
||||
if ($customId) {
|
||||
[$tenantId, $packageId, $type] = explode('_', $customId);
|
||||
$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 ?? 'endcustomer_event',
|
||||
'purchased_at' => now(),
|
||||
'refunded' => false,
|
||||
]);
|
||||
|
||||
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);
|
||||
|
||||
$subscriptionsController = $this->client->getSubscriptionsController();
|
||||
|
||||
$requestBody = SubscriptionRequestBuilder::init()
|
||||
->planId($request->plan_id)
|
||||
->subscriber(
|
||||
SubscriberBuilder::init()
|
||||
->name(
|
||||
NameBuilder::init()
|
||||
->givenName($tenant->name ?? 'Tenant')
|
||||
->build()
|
||||
)
|
||||
->emailAddress($tenant->email)
|
||||
->build()
|
||||
)
|
||||
->customId($tenant->id . '_' . $package->id . '_reseller_subscription')
|
||||
->applicationContext(
|
||||
ApplicationContextSubscriptionBuilder::init()
|
||||
->shippingPreference(ShippingPreference::NO_SHIPPING)
|
||||
->userAction('SUBSCRIBE_NOW')
|
||||
->build()
|
||||
)
|
||||
->build();
|
||||
|
||||
$collect = [
|
||||
'body' => $requestBody,
|
||||
'prefer' => 'return=representation'
|
||||
];
|
||||
|
||||
try {
|
||||
$response = $subscriptionsController->createSubscription($collect);
|
||||
|
||||
if ($response->statusCode === 201) {
|
||||
$result = $response->result;
|
||||
$subscriptionId = $result->id;
|
||||
|
||||
TenantPackage::create([
|
||||
'tenant_id' => $tenant->id,
|
||||
'package_id' => $package->id,
|
||||
'purchased_at' => now(),
|
||||
'expires_at' => now()->addYear(),
|
||||
'active' => true,
|
||||
]);
|
||||
|
||||
PackagePurchase::create([
|
||||
'tenant_id' => $tenant->id,
|
||||
'package_id' => $package->id,
|
||||
'provider_id' => $subscriptionId,
|
||||
'price' => $package->price,
|
||||
'type' => 'reseller_subscription',
|
||||
'purchased_at' => now(),
|
||||
]);
|
||||
|
||||
$approveLink = collect($result->links)->first(fn($link) => $link->rel === 'approve')?->href;
|
||||
|
||||
return response()->json([
|
||||
'subscription_id' => $subscriptionId,
|
||||
'approve_url' => $approveLink,
|
||||
]);
|
||||
}
|
||||
|
||||
Log::error('PayPal subscription creation failed', ['response' => $response]);
|
||||
return response()->json(['error' => 'Subscription creation failed'], 400);
|
||||
} catch (\Exception $e) {
|
||||
Log::error('PayPal subscription creation exception', ['error' => $e->getMessage()]);
|
||||
return response()->json(['error' => 'Subscription creation failed'], 500);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -16,32 +16,60 @@ class PayPalWebhookController extends Controller
|
||||
$payerEmail = $input['payer_email'] ?? null;
|
||||
$paymentStatus = $input['payment_status'] ?? null;
|
||||
$mcGross = $input['mc_gross'] ?? 0;
|
||||
$packageId = $input['custom'] ?? null;
|
||||
$custom = $input['custom'] ?? null;
|
||||
|
||||
if ($paymentStatus === 'Completed' && $mcGross > 0) {
|
||||
// Verify IPN with PayPal (simplified; use SDK for full verification)
|
||||
// $verified = $this->verifyIPN($input);
|
||||
|
||||
// Find or create tenant (for public checkout, perhaps create new or use session)
|
||||
// For now, assume tenant_id from custom or session
|
||||
$tenantId = $packageId ? Tenant::where('slug', $packageId)->first()->id ?? 1 : 1;
|
||||
// Parse custom for user_id or tenant_id
|
||||
$data = json_decode($custom, true);
|
||||
$userId = $data['user_id'] ?? null;
|
||||
$tenantId = $data['tenant_id'] ?? null;
|
||||
$packageId = $data['package_id'] ?? null;
|
||||
|
||||
// Create purchase and increment credits
|
||||
$purchase = EventPurchase::create([
|
||||
'tenant_id' => $tenantId, // Implement tenant resolution
|
||||
'events_purchased' => $mcGross / 49, // Example: 49€ per event credit
|
||||
'amount' => $mcGross,
|
||||
'currency' => $input['mc_currency'] ?? 'EUR',
|
||||
'provider' => 'paypal',
|
||||
'external_receipt_id' => $ipnMessage,
|
||||
'status' => 'completed',
|
||||
if ($userId && !$tenantId) {
|
||||
$tenant = \App\Models\Tenant::where('user_id', $userId)->first();
|
||||
if ($tenant) {
|
||||
$tenantId = $tenant->id;
|
||||
} else {
|
||||
Log::error('Tenant not found for user_id in PayPal IPN: ' . $userId);
|
||||
return response('OK', 200);
|
||||
}
|
||||
}
|
||||
|
||||
if (!$tenantId || !$packageId) {
|
||||
Log::error('Missing tenant or package in PayPal IPN custom data');
|
||||
return response('OK', 200);
|
||||
}
|
||||
|
||||
// Create PackagePurchase
|
||||
\App\Models\PackagePurchase::create([
|
||||
'tenant_id' => $tenantId,
|
||||
'package_id' => $packageId,
|
||||
'provider_id' => $ipnMessage,
|
||||
'price' => $mcGross,
|
||||
'type' => $data['type'] ?? 'reseller_subscription',
|
||||
'purchased_at' => now(),
|
||||
'refunded' => false,
|
||||
]);
|
||||
|
||||
$tenant = Tenant::find($tenantId);
|
||||
$tenant->incrementCredits($purchase->events_purchased, 'paypal_purchase', 'PayPal IPN', $purchase->id);
|
||||
// Update TenantPackage if subscription
|
||||
if ($data['type'] ?? '' === 'reseller_subscription') {
|
||||
\App\Models\TenantPackage::updateOrCreate(
|
||||
[
|
||||
'tenant_id' => $tenantId,
|
||||
'package_id' => $packageId,
|
||||
],
|
||||
[
|
||||
'active' => true,
|
||||
'purchased_at' => now(),
|
||||
'expires_at' => now()->addYear(),
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
Log::info('PayPal IPN processed', $input);
|
||||
Log::info('PayPal IPN processed for tenant ' . $tenantId . ', package ' . $packageId, $input);
|
||||
}
|
||||
|
||||
return response('OK', 200);
|
||||
|
||||
67
app/Http/Controllers/ProfileController.php
Normal file
67
app/Http/Controllers/ProfileController.php
Normal file
@@ -0,0 +1,67 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Models\User;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Illuminate\Support\Facades\Hash;
|
||||
use Illuminate\Validation\Rules;
|
||||
use Illuminate\View\View;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
|
||||
class ProfileController extends Controller
|
||||
{
|
||||
/**
|
||||
* Display the user's profile form.
|
||||
*/
|
||||
public function edit(Request $request): View
|
||||
{
|
||||
return view('profile.edit', [
|
||||
'user' => $request->user(),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the user's profile information.
|
||||
*/
|
||||
public function update(Request $request, User $user): RedirectResponse
|
||||
{
|
||||
// Authorized via auth middleware
|
||||
|
||||
$request->validate([
|
||||
'name' => 'required|string|max:255',
|
||||
'username' => ['required', 'string', 'max:255', 'alpha_dash', 'unique:users,username,' . $user->id],
|
||||
'email' => ['required', 'string', 'email', 'max:255', 'unique:users,email,' . $user->id],
|
||||
'first_name' => ['required', 'string', 'max:255'],
|
||||
'last_name' => ['required', 'string', 'max:255'],
|
||||
'address' => ['required', 'string'],
|
||||
'phone' => ['required', 'string', 'max:20'],
|
||||
]);
|
||||
|
||||
$user->update($request->only([
|
||||
'name', 'username', 'email', 'first_name', 'last_name', 'address', 'phone'
|
||||
]));
|
||||
|
||||
return back()->with('status', 'profile-updated');
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the user's password.
|
||||
*/
|
||||
public function updatePassword(Request $request, User $user): RedirectResponse
|
||||
{
|
||||
// Authorized via auth middleware
|
||||
|
||||
$request->validate([
|
||||
'current_password' => ['required', 'current_password'],
|
||||
'password' => ['required', 'confirmed', Rules\Password::defaults()],
|
||||
]);
|
||||
|
||||
$user->update([
|
||||
'password' => Hash::make($request->password),
|
||||
]);
|
||||
|
||||
return back()->with('status', 'password-updated');
|
||||
}
|
||||
}
|
||||
107
app/Http/Controllers/StripeController.php
Normal file
107
app/Http/Controllers/StripeController.php
Normal file
@@ -0,0 +1,107 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Models\PackagePurchase;
|
||||
use App\Models\Tenant;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
use Stripe\Stripe;
|
||||
use Stripe\PaymentIntent;
|
||||
use Stripe\Subscription;
|
||||
|
||||
class StripeController extends Controller
|
||||
{
|
||||
public function __construct()
|
||||
{
|
||||
Stripe::setApiKey(config('services.stripe.secret'));
|
||||
}
|
||||
|
||||
public function createPaymentIntent(Request $request)
|
||||
{
|
||||
$request->validate([
|
||||
'tenant_id' => 'required|exists:tenants,id',
|
||||
'package_id' => 'required|exists:packages,id',
|
||||
'amount' => 'required|numeric|min:0',
|
||||
]);
|
||||
|
||||
$tenant = Tenant::findOrFail($request->tenant_id);
|
||||
$package = \App\Models\Package::findOrFail($request->package_id);
|
||||
|
||||
$paymentIntent = PaymentIntent::create([
|
||||
'amount' => $request->amount * 100, // cents
|
||||
'currency' => 'eur',
|
||||
'metadata' => [
|
||||
'tenant_id' => $tenant->id,
|
||||
'package_id' => $package->id,
|
||||
'type' => 'endcustomer_event', // or reseller
|
||||
],
|
||||
]);
|
||||
|
||||
return response()->json([
|
||||
'client_secret' => $paymentIntent->client_secret,
|
||||
]);
|
||||
}
|
||||
|
||||
public function createSubscription(Request $request)
|
||||
{
|
||||
$request->validate([
|
||||
'tenant_id' => 'required|exists:tenants,id',
|
||||
'package_id' => 'required|exists:packages,id',
|
||||
'payment_method_id' => 'required',
|
||||
]);
|
||||
|
||||
$tenant = Tenant::findOrFail($request->tenant_id);
|
||||
$package = \App\Models\Package::findOrFail($request->package_id);
|
||||
|
||||
$subscription = Subscription::create([
|
||||
'customer' => $tenant->stripe_customer_id ?? $this->createCustomer($tenant),
|
||||
'items' => [[
|
||||
'price' => $package->stripe_price_id, // Assume package has stripe_price_id
|
||||
]],
|
||||
'payment_method' => $request->payment_method_id,
|
||||
'default_payment_method' => $request->payment_method_id,
|
||||
'expand' => ['latest_invoice.payment_intent'],
|
||||
'metadata' => [
|
||||
'tenant_id' => $tenant->id,
|
||||
'package_id' => $package->id,
|
||||
'type' => 'reseller_subscription',
|
||||
],
|
||||
]);
|
||||
|
||||
// Create TenantPackage and PackagePurchase
|
||||
\App\Models\TenantPackage::create([
|
||||
'tenant_id' => $tenant->id,
|
||||
'package_id' => $package->id,
|
||||
'purchased_at' => now(),
|
||||
'expires_at' => now()->addYear(),
|
||||
'active' => true,
|
||||
]);
|
||||
|
||||
PackagePurchase::create([
|
||||
'tenant_id' => $tenant->id,
|
||||
'package_id' => $package->id,
|
||||
'provider_id' => $subscription->id,
|
||||
'price' => $package->price,
|
||||
'type' => 'reseller_subscription',
|
||||
'purchased_at' => now(),
|
||||
]);
|
||||
|
||||
return response()->json([
|
||||
'subscription_id' => $subscription->id,
|
||||
'client_secret' => $subscription->latest_invoice->payment_intent->client_secret ?? null,
|
||||
]);
|
||||
}
|
||||
|
||||
private function createCustomer(Tenant $tenant)
|
||||
{
|
||||
$customer = \Stripe\Customer::create([
|
||||
'email' => $tenant->email,
|
||||
'metadata' => ['tenant_id' => $tenant->id],
|
||||
]);
|
||||
|
||||
$tenant->update(['stripe_customer_id' => $customer->id]);
|
||||
|
||||
return $customer->id;
|
||||
}
|
||||
}
|
||||
@@ -2,90 +2,169 @@
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Models\EventPurchase;
|
||||
use App\Models\Tenant;
|
||||
use App\Models\PackagePurchase;
|
||||
use App\Models\TenantPackage;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
use Stripe\Stripe;
|
||||
use Stripe\Webhook;
|
||||
|
||||
class StripeWebhookController extends Controller
|
||||
{
|
||||
public function handle(Request $request)
|
||||
public function __construct()
|
||||
{
|
||||
Stripe::setApiKey(config('services.stripe.secret'));
|
||||
}
|
||||
|
||||
public function handleWebhook(Request $request)
|
||||
{
|
||||
$payload = $request->getContent();
|
||||
$sig = $request->header('Stripe-Signature');
|
||||
$secret = config('services.stripe.webhook');
|
||||
$sigHeader = $request->header('Stripe-Signature');
|
||||
$endpointSecret = config('services.stripe.webhook_secret');
|
||||
|
||||
if (!$secret || !$sig) {
|
||||
abort(400, 'Missing signature');
|
||||
try {
|
||||
$event = Webhook::constructEvent($payload, $sigHeader, $endpointSecret);
|
||||
} catch (\Exception $e) {
|
||||
Log::error('Stripe webhook signature verification failed: ' . $e->getMessage());
|
||||
return response()->json(['error' => 'Invalid signature'], 400);
|
||||
}
|
||||
|
||||
$expectedSig = 'v1=' . hash_hmac('sha256', $payload, $secret);
|
||||
switch ($event['type']) {
|
||||
case 'payment_intent.succeeded':
|
||||
$paymentIntent = $event['data']['object'];
|
||||
$this->handlePaymentIntentSucceeded($paymentIntent);
|
||||
break;
|
||||
|
||||
if (!hash_equals($expectedSig, $sig)) {
|
||||
abort(400, 'Invalid signature');
|
||||
case 'invoice.payment_succeeded':
|
||||
$invoice = $event['data']['object'];
|
||||
$this->handleInvoicePaymentSucceeded($invoice);
|
||||
break;
|
||||
|
||||
case 'invoice.payment_failed':
|
||||
$invoice = $event['data']['object'];
|
||||
$this->handleInvoicePaymentFailed($invoice);
|
||||
break;
|
||||
|
||||
default:
|
||||
Log::info('Unhandled Stripe event type: ' . $event['type']);
|
||||
}
|
||||
|
||||
$event = json_decode($payload, true);
|
||||
return response()->json(['status' => 'success']);
|
||||
}
|
||||
|
||||
if (json_last_error() !== JSON_ERROR_NONE) {
|
||||
Log::error('Invalid JSON in Stripe webhook: ' . json_last_error_msg());
|
||||
return response('', 200);
|
||||
private function handlePaymentIntentSucceeded($paymentIntent)
|
||||
{
|
||||
$metadata = $paymentIntent['metadata'];
|
||||
if (!$metadata['user_id'] && !$metadata['tenant_id'] || !$metadata['package_id']) {
|
||||
Log::warning('Missing metadata in Stripe payment intent: ' . $paymentIntent['id']);
|
||||
return;
|
||||
}
|
||||
|
||||
if ($event['type'] === 'checkout.session.completed') {
|
||||
$session = $event['data']['object'];
|
||||
$receiptId = $session['id'];
|
||||
$userId = $metadata['user_id'] ?? null;
|
||||
$tenantId = $metadata['tenant_id'] ?? null;
|
||||
$packageId = $metadata['package_id'];
|
||||
$type = $metadata['type'] ?? 'endcustomer_event';
|
||||
|
||||
// Idempotency check
|
||||
if (EventPurchase::where('external_receipt_id', $receiptId)->exists()) {
|
||||
return response('', 200);
|
||||
if ($userId && !$tenantId) {
|
||||
$tenant = \App\Models\Tenant::where('user_id', $userId)->first();
|
||||
if ($tenant) {
|
||||
$tenantId = $tenant->id;
|
||||
} else {
|
||||
Log::error('Tenant not found for user_id: ' . $userId);
|
||||
return;
|
||||
}
|
||||
|
||||
$tenantId = $session['metadata']['tenant_id'] ?? null;
|
||||
|
||||
if (!$tenantId) {
|
||||
Log::warning('No tenant_id in Stripe metadata', ['receipt_id' => $receiptId]);
|
||||
// Dispatch job for retry or manual resolution
|
||||
\App\Jobs\ValidateStripeWebhookJob::dispatch($payload, $sig);
|
||||
return response('', 200);
|
||||
}
|
||||
|
||||
$tenant = Tenant::find($tenantId);
|
||||
|
||||
if (!$tenant) {
|
||||
Log::error('Tenant not found for Stripe webhook', ['tenant_id' => $tenantId]);
|
||||
\App\Jobs\ValidateStripeWebhookJob::dispatch($payload, $sig);
|
||||
return response('', 200);
|
||||
}
|
||||
|
||||
$amount = $session['amount_total'] / 100;
|
||||
$currency = $session['currency'];
|
||||
$eventsPurchased = (int) ($session['metadata']['events_purchased'] ?? 1);
|
||||
|
||||
DB::transaction(function () use ($tenant, $amount, $currency, $eventsPurchased, $receiptId) {
|
||||
$purchase = EventPurchase::create([
|
||||
'tenant_id' => $tenant->id,
|
||||
'events_purchased' => $eventsPurchased,
|
||||
'amount' => $amount,
|
||||
'currency' => $currency,
|
||||
'provider' => 'stripe',
|
||||
'external_receipt_id' => $receiptId,
|
||||
'status' => 'completed',
|
||||
'purchased_at' => now(),
|
||||
]);
|
||||
|
||||
$tenant->incrementCredits($eventsPurchased, 'purchase', null, $purchase->id);
|
||||
});
|
||||
|
||||
Log::info('Processed Stripe purchase', ['receipt_id' => $receiptId, 'tenant_id' => $tenantId]);
|
||||
} else {
|
||||
// For other event types, log or dispatch job if needed
|
||||
Log::info('Unhandled Stripe event', ['type' => $event['type']]);
|
||||
// Optionally dispatch job for processing other events
|
||||
\App\Jobs\ValidateStripeWebhookJob::dispatch($payload, $sig);
|
||||
}
|
||||
|
||||
return response('', 200);
|
||||
if (!$tenantId) {
|
||||
Log::error('No tenant_id found for Stripe payment intent: ' . $paymentIntent['id']);
|
||||
return;
|
||||
}
|
||||
|
||||
// Create PackagePurchase for one-off payment
|
||||
\App\Models\PackagePurchase::create([
|
||||
'tenant_id' => $tenantId,
|
||||
'package_id' => $packageId,
|
||||
'provider_id' => $paymentIntent['id'],
|
||||
'price' => $paymentIntent['amount_received'] / 100,
|
||||
'type' => $type,
|
||||
'purchased_at' => now(),
|
||||
'refunded' => false,
|
||||
]);
|
||||
|
||||
if ($type === 'endcustomer_event') {
|
||||
// For event packages, assume event_id from metadata or handle separately
|
||||
// TODO: Link to specific event if provided
|
||||
}
|
||||
|
||||
Log::info('Package purchase created via Stripe payment intent: ' . $paymentIntent['id'] . ' for tenant ' . $tenantId);
|
||||
}
|
||||
|
||||
private function handleInvoicePaymentSucceeded($invoice)
|
||||
{
|
||||
$subscription = $invoice['subscription'];
|
||||
$metadata = $invoice['metadata'];
|
||||
|
||||
if (!$metadata['user_id'] && !$metadata['tenant_id'] || !$metadata['package_id']) {
|
||||
Log::warning('Missing metadata in Stripe invoice: ' . $invoice['id']);
|
||||
return;
|
||||
}
|
||||
|
||||
$userId = $metadata['user_id'] ?? null;
|
||||
$tenantId = $metadata['tenant_id'] ?? null;
|
||||
$packageId = $metadata['package_id'];
|
||||
|
||||
if ($userId && !$tenantId) {
|
||||
$tenant = \App\Models\Tenant::where('user_id', $userId)->first();
|
||||
if ($tenant) {
|
||||
$tenantId = $tenant->id;
|
||||
} else {
|
||||
Log::error('Tenant not found for user_id: ' . $userId);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (!$tenantId) {
|
||||
Log::error('No tenant_id found for Stripe invoice: ' . $invoice['id']);
|
||||
return;
|
||||
}
|
||||
|
||||
// Update or create TenantPackage for subscription
|
||||
\App\Models\TenantPackage::updateOrCreate(
|
||||
[
|
||||
'tenant_id' => $tenantId,
|
||||
'package_id' => $packageId,
|
||||
],
|
||||
[
|
||||
'purchased_at' => now(),
|
||||
'expires_at' => now()->addYear(), // Renew annually
|
||||
'active' => true,
|
||||
]
|
||||
);
|
||||
|
||||
// Create or update PackagePurchase
|
||||
\App\Models\PackagePurchase::updateOrCreate(
|
||||
[
|
||||
'tenant_id' => $tenantId,
|
||||
'package_id' => $packageId,
|
||||
'provider_id' => $subscription,
|
||||
],
|
||||
[
|
||||
'price' => $invoice['amount_paid'] / 100,
|
||||
'type' => 'reseller_subscription',
|
||||
'purchased_at' => now(),
|
||||
'refunded' => false,
|
||||
]
|
||||
);
|
||||
|
||||
Log::info('Subscription renewed via Stripe invoice: ' . $invoice['id'] . ' for tenant ' . $tenantId);
|
||||
}
|
||||
|
||||
private function handleInvoicePaymentFailed($invoice)
|
||||
{
|
||||
$subscription = $invoice['subscription'];
|
||||
Log::warning('Stripe invoice payment failed: ' . $invoice['id'] . ' for subscription ' . $subscription);
|
||||
|
||||
// TODO: Deactivate package or notify tenant
|
||||
// e.g., TenantPackage::where('provider_id', $subscription)->update(['active' => false]);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user