switched to paddle inline checkout, removed paypal and most of stripe. added product sync between app and paddle.
This commit is contained in:
@@ -5,21 +5,17 @@ namespace App\Http\Controllers\Api;
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\Package;
|
||||
use App\Models\PackagePurchase;
|
||||
use App\Models\Tenant;
|
||||
use App\Models\TenantPackage;
|
||||
use App\Services\Paddle\PaddleCheckoutService;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
use Illuminate\Validation\ValidationException;
|
||||
use PayPal\Checkout\Orders\OrdersCaptureRequest;
|
||||
use PayPal\Checkout\Orders\OrdersCreateRequest;
|
||||
use PayPal\Environment\LiveEnvironment;
|
||||
use PayPal\Environment\SandboxEnvironment;
|
||||
use PayPal\PayPalClient;
|
||||
|
||||
class PackageController extends Controller
|
||||
{
|
||||
public function __construct(private readonly PaddleCheckoutService $paddleCheckout) {}
|
||||
|
||||
public function index(Request $request): JsonResponse
|
||||
{
|
||||
$type = $request->query('type', 'endcustomer');
|
||||
@@ -51,7 +47,7 @@ class PackageController extends Controller
|
||||
$request->validate([
|
||||
'package_id' => 'required|exists:packages,id',
|
||||
'type' => 'required|in:endcustomer,reseller',
|
||||
'payment_method' => 'required|in:stripe,paypal',
|
||||
'payment_method' => 'required|in:stripe,paddle',
|
||||
'event_id' => 'nullable|exists:events,id', // For endcustomer
|
||||
]);
|
||||
|
||||
@@ -105,8 +101,8 @@ class PackageController extends Controller
|
||||
{
|
||||
$request->validate([
|
||||
'package_id' => 'required|exists:packages,id',
|
||||
'payment_method_id' => 'required_without:paypal_order_id|string',
|
||||
'paypal_order_id' => 'required_without:payment_method_id|string',
|
||||
'payment_method_id' => 'required_without:paddle_transaction_id|string',
|
||||
'paddle_transaction_id' => 'required_without:payment_method_id|string',
|
||||
]);
|
||||
|
||||
$package = Package::findOrFail($request->package_id);
|
||||
@@ -116,13 +112,14 @@ class PackageController extends Controller
|
||||
throw ValidationException::withMessages(['tenant' => 'Tenant not found.']);
|
||||
}
|
||||
|
||||
$provider = $request->has('paypal_order_id') ? 'paypal' : 'stripe';
|
||||
$provider = $request->has('paddle_transaction_id') ? 'paddle' : 'stripe';
|
||||
|
||||
DB::transaction(function () use ($request, $package, $tenant, $provider) {
|
||||
PackagePurchase::create([
|
||||
'tenant_id' => $tenant->id,
|
||||
'package_id' => $package->id,
|
||||
'provider_id' => $request->input($provider === 'paypal' ? 'paypal_order_id' : 'payment_method_id'),
|
||||
'provider' => $provider,
|
||||
'provider_id' => $request->input($provider === 'paddle' ? 'paddle_transaction_id' : 'payment_method_id'),
|
||||
'price' => $package->price,
|
||||
'type' => 'endcustomer_event',
|
||||
'purchased_at' => now(),
|
||||
@@ -165,6 +162,7 @@ class PackageController extends Controller
|
||||
PackagePurchase::create([
|
||||
'tenant_id' => $tenant->id,
|
||||
'package_id' => $package->id,
|
||||
'provider' => 'free',
|
||||
'provider_id' => 'free_wizard',
|
||||
'price' => $package->price,
|
||||
'type' => 'endcustomer_event',
|
||||
@@ -186,156 +184,33 @@ class PackageController extends Controller
|
||||
], 201);
|
||||
}
|
||||
|
||||
public function createPayPalOrder(Request $request): JsonResponse
|
||||
public function createPaddleCheckout(Request $request): JsonResponse
|
||||
{
|
||||
$request->validate([
|
||||
'package_id' => 'required|exists:packages,id',
|
||||
'success_url' => 'nullable|url',
|
||||
'return_url' => 'nullable|url',
|
||||
]);
|
||||
|
||||
$package = Package::findOrFail($request->package_id);
|
||||
$package = Package::findOrFail($request->integer('package_id'));
|
||||
$tenant = $request->attributes->get('tenant');
|
||||
|
||||
if (! $tenant) {
|
||||
throw ValidationException::withMessages(['tenant' => 'Tenant not found.']);
|
||||
throw ValidationException::withMessages(['tenant' => 'Tenant context missing.']);
|
||||
}
|
||||
|
||||
$environment = config('services.paypal.sandbox', true) ? new SandboxEnvironment(
|
||||
config('services.paypal.client_id'),
|
||||
config('services.paypal.secret')
|
||||
) : new LiveEnvironment(
|
||||
config('services.paypal.client_id'),
|
||||
config('services.paypal.secret')
|
||||
);
|
||||
if (! $package->paddle_price_id) {
|
||||
throw ValidationException::withMessages(['package_id' => 'Package is not linked to a Paddle price.']);
|
||||
}
|
||||
|
||||
$client = PayPalClient::client($environment);
|
||||
|
||||
$request = new OrdersCreateRequest;
|
||||
$request->prefer('return=representation');
|
||||
$request->body = [
|
||||
'intent' => 'CAPTURE',
|
||||
'purchase_units' => [[
|
||||
'amount' => [
|
||||
'currency_code' => 'EUR',
|
||||
'value' => number_format($package->price, 2, '.', ''),
|
||||
],
|
||||
'description' => 'Fotospiel Package: '.$package->name,
|
||||
'custom_id' => json_encode([
|
||||
'tenant_id' => $tenant->id,
|
||||
'package_id' => $package->id,
|
||||
'user_id' => $tenant->user_id ?? null,
|
||||
]),
|
||||
]],
|
||||
'application_context' => [
|
||||
'shipping_preference' => 'NO_SHIPPING',
|
||||
'user_action' => 'PAY_NOW',
|
||||
],
|
||||
$payload = [
|
||||
'success_url' => $request->input('success_url'),
|
||||
'return_url' => $request->input('return_url'),
|
||||
];
|
||||
|
||||
try {
|
||||
$response = $client->execute($request);
|
||||
$order = $response->result;
|
||||
$checkout = $this->paddleCheckout->createCheckout($tenant, $package, $payload);
|
||||
|
||||
return response()->json([
|
||||
'orderID' => $order->id,
|
||||
]);
|
||||
} catch (\Exception $e) {
|
||||
Log::error('PayPal order creation error: '.$e->getMessage());
|
||||
throw ValidationException::withMessages(['payment' => 'PayPal-Bestellung fehlgeschlagen.']);
|
||||
}
|
||||
}
|
||||
|
||||
public function capturePayPalOrder(Request $request): JsonResponse
|
||||
{
|
||||
$request->validate([
|
||||
'order_id' => 'required|string',
|
||||
]);
|
||||
|
||||
$orderId = $request->order_id;
|
||||
|
||||
$environment = config('services.paypal.sandbox', true) ? new SandboxEnvironment(
|
||||
config('services.paypal.client_id'),
|
||||
config('services.paypal.secret')
|
||||
) : new LiveEnvironment(
|
||||
config('services.paypal.client_id'),
|
||||
config('services.paypal.secret')
|
||||
);
|
||||
|
||||
$client = PayPalClient::client($environment);
|
||||
|
||||
$request = new OrdersCaptureRequest($orderId);
|
||||
$request->prefer('return=representation');
|
||||
|
||||
try {
|
||||
$response = $client->execute($request);
|
||||
$capture = $response->result;
|
||||
|
||||
if ($capture->status !== 'COMPLETED') {
|
||||
throw new \Exception('PayPal capture not completed: '.$capture->status);
|
||||
}
|
||||
|
||||
$customId = $capture->purchaseUnits[0]->customId ?? null;
|
||||
if (! $customId) {
|
||||
throw new \Exception('No metadata in PayPal order');
|
||||
}
|
||||
|
||||
$metadata = json_decode($customId, true);
|
||||
$tenant = Tenant::find($metadata['tenant_id']);
|
||||
$package = Package::find($metadata['package_id']);
|
||||
|
||||
if (! $tenant || ! $package) {
|
||||
throw new \Exception('Tenant or package not found');
|
||||
}
|
||||
|
||||
// Idempotent check
|
||||
$existing = PackagePurchase::where('provider_id', $orderId)->first();
|
||||
if ($existing) {
|
||||
return response()->json(['success' => true, 'message' => 'Already processed']);
|
||||
}
|
||||
|
||||
DB::transaction(function () use ($tenant, $package, $orderId) {
|
||||
PackagePurchase::create([
|
||||
'tenant_id' => $tenant->id,
|
||||
'package_id' => $package->id,
|
||||
'provider_id' => $orderId,
|
||||
'price' => $package->price,
|
||||
'type' => $package->type === 'endcustomer' ? 'endcustomer_event' : 'reseller_subscription',
|
||||
'purchased_at' => now(),
|
||||
'metadata' => json_encode(['paypal_order' => $orderId]),
|
||||
]);
|
||||
|
||||
// Trial logic for first reseller subscription
|
||||
$activePackages = TenantPackage::where('tenant_id', $tenant->id)
|
||||
->where('active', true)
|
||||
->where('package_id', '!=', $package->id) // Exclude current if renewing
|
||||
->count();
|
||||
|
||||
$expiresAt = now()->addYear();
|
||||
if ($activePackages === 0 && $package->type === 'reseller_subscription') {
|
||||
$expiresAt = now()->addDays(14); // 14-day trial
|
||||
Log::info('PayPal trial activated for tenant', ['tenant_id' => $tenant->id]);
|
||||
}
|
||||
|
||||
TenantPackage::updateOrCreate(
|
||||
['tenant_id' => $tenant->id, 'package_id' => $package->id],
|
||||
[
|
||||
'price' => $package->price,
|
||||
'purchased_at' => now(),
|
||||
'active' => true,
|
||||
'expires_at' => $expiresAt,
|
||||
]
|
||||
);
|
||||
|
||||
$tenant->update(['subscription_status' => 'active']);
|
||||
});
|
||||
|
||||
Log::info('PayPal order captured successfully', ['order_id' => $orderId, 'tenant_id' => $tenant->id]);
|
||||
|
||||
return response()->json(['success' => true, 'message' => 'Payment successful']);
|
||||
} catch (\Exception $e) {
|
||||
Log::error('PayPal capture error: '.$e->getMessage(), ['order_id' => $orderId]);
|
||||
|
||||
return response()->json(['success' => false, 'message' => 'Capture failed: '.$e->getMessage()], 422);
|
||||
}
|
||||
return response()->json($checkout);
|
||||
}
|
||||
|
||||
private function handleFreePurchase(Request $request, Package $package, $tenant): JsonResponse
|
||||
@@ -345,6 +220,7 @@ class PackageController extends Controller
|
||||
'tenant_id' => $tenant->id,
|
||||
'event_id' => $request->event_id,
|
||||
'package_id' => $package->id,
|
||||
'provider' => 'free',
|
||||
'provider_id' => 'free',
|
||||
'price' => $package->price,
|
||||
'type' => $request->type,
|
||||
@@ -397,7 +273,4 @@ class PackageController extends Controller
|
||||
return $response;
|
||||
}
|
||||
}
|
||||
|
||||
// Helper for PayPal client - add this if not exists, or use global
|
||||
// But since SDK has PayPalClient, assume it's used
|
||||
}
|
||||
|
||||
63
app/Http/Controllers/Api/TenantBillingController.php
Normal file
63
app/Http/Controllers/Api/TenantBillingController.php
Normal file
@@ -0,0 +1,63 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Api;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Services\Paddle\PaddleTransactionService;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
|
||||
class TenantBillingController extends Controller
|
||||
{
|
||||
public function __construct(private readonly PaddleTransactionService $paddleTransactions) {}
|
||||
|
||||
public function transactions(Request $request): JsonResponse
|
||||
{
|
||||
$tenant = $request->attributes->get('tenant');
|
||||
|
||||
if (! $tenant) {
|
||||
return response()->json([
|
||||
'data' => [],
|
||||
'message' => 'Tenant not found.',
|
||||
], 404);
|
||||
}
|
||||
|
||||
if (! $tenant->paddle_customer_id) {
|
||||
return response()->json([
|
||||
'data' => [],
|
||||
'message' => 'Tenant has no Paddle customer identifier.',
|
||||
]);
|
||||
}
|
||||
|
||||
$cursor = $request->query('cursor');
|
||||
$perPage = (int) $request->query('per_page', 25);
|
||||
|
||||
$query = [
|
||||
'per_page' => max(1, min($perPage, 100)),
|
||||
];
|
||||
|
||||
if ($cursor) {
|
||||
$query['after'] = $cursor;
|
||||
}
|
||||
|
||||
try {
|
||||
$result = $this->paddleTransactions->listForCustomer($tenant->paddle_customer_id, $query);
|
||||
} catch (\Throwable $exception) {
|
||||
Log::warning('Failed to load Paddle transactions', [
|
||||
'tenant_id' => $tenant->id,
|
||||
'error' => $exception->getMessage(),
|
||||
]);
|
||||
|
||||
return response()->json([
|
||||
'data' => [],
|
||||
'message' => 'Failed to load Paddle transactions.',
|
||||
], 502);
|
||||
}
|
||||
|
||||
return response()->json([
|
||||
'data' => $result['data'],
|
||||
'meta' => $result['meta'],
|
||||
]);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user