feat: implement comprehensive multi-provider payment processing system
- Add unified payment provider architecture with contract-based design - Implement 6 payment providers: Stripe, Lemon Squeezy, Polar, Oxapay, Crypto, Activation Keys - Create subscription management with lifecycle handling (create, cancel, pause, resume, update) - Add coupon system with usage tracking and trial extensions - Build Filament admin resources for payment providers, subscriptions, coupons, and trials - Implement payment orchestration service with provider registry and configuration management - Add comprehensive payment logging and webhook handling for all providers - Create customer analytics dashboard with revenue, churn, and lifetime value metrics - Add subscription migration service for provider switching - Include extensive test coverage for all payment functionality
This commit is contained in:
240
app/Http/Controllers/PaymentController.php
Normal file
240
app/Http/Controllers/PaymentController.php
Normal file
@@ -0,0 +1,240 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Models\Plan;
|
||||
use App\Models\User;
|
||||
use App\Services\Payments\PaymentOrchestrator;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Validation\ValidationException;
|
||||
|
||||
class PaymentController extends Controller
|
||||
{
|
||||
public function __construct(
|
||||
private PaymentOrchestrator $orchestrator
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Create a checkout session for a plan
|
||||
*/
|
||||
public function createCheckout(Request $request): JsonResponse
|
||||
{
|
||||
try {
|
||||
$validated = $request->validate([
|
||||
'plan_id' => 'required|exists:plans,id',
|
||||
'provider' => 'nullable|string|in:stripe,lemon_squeezy,polar,oxapay,crypto,activation_key',
|
||||
'options' => 'nullable|array',
|
||||
]);
|
||||
|
||||
$user = $request->user();
|
||||
$plan = Plan::findOrFail($validated['plan_id']);
|
||||
$provider = $validated['provider'] ?? null;
|
||||
$options = $validated['options'] ?? [];
|
||||
|
||||
$result = $this->orchestrator->createCheckoutSession($user, $plan, $provider, $options);
|
||||
|
||||
return response()->json([
|
||||
'success' => true,
|
||||
'data' => $result,
|
||||
]);
|
||||
|
||||
} catch (ValidationException $e) {
|
||||
return response()->json([
|
||||
'success' => false,
|
||||
'errors' => $e->errors(),
|
||||
], 422);
|
||||
} catch (\Exception $e) {
|
||||
return response()->json([
|
||||
'success' => false,
|
||||
'error' => $e->getMessage(),
|
||||
], 500);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new subscription
|
||||
*/
|
||||
public function createSubscription(Request $request): JsonResponse
|
||||
{
|
||||
try {
|
||||
$validated = $request->validate([
|
||||
'plan_id' => 'required|exists:plans,id',
|
||||
'provider' => 'nullable|string|in:stripe,lemon_squeezy,polar',
|
||||
'options' => 'nullable|array',
|
||||
]);
|
||||
|
||||
$user = $request->user();
|
||||
$plan = Plan::findOrFail($validated['plan_id']);
|
||||
$provider = $validated['provider'] ?? null;
|
||||
$options = $validated['options'] ?? [];
|
||||
|
||||
// Only recurring providers can create subscriptions
|
||||
if (! $plan->monthly_billing) {
|
||||
return response()->json([
|
||||
'success' => false,
|
||||
'error' => 'This plan does not support recurring subscriptions. Use checkout instead.',
|
||||
], 400);
|
||||
}
|
||||
|
||||
$result = $this->orchestrator->createSubscription($user, $plan, $provider, $options);
|
||||
|
||||
return response()->json([
|
||||
'success' => true,
|
||||
'data' => $result,
|
||||
]);
|
||||
|
||||
} catch (ValidationException $e) {
|
||||
return response()->json([
|
||||
'success' => false,
|
||||
'errors' => $e->errors(),
|
||||
], 422);
|
||||
} catch (\Exception $e) {
|
||||
return response()->json([
|
||||
'success' => false,
|
||||
'error' => $e->getMessage(),
|
||||
], 500);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get available payment methods for a plan
|
||||
*/
|
||||
public function getPaymentMethods(Request $request): JsonResponse
|
||||
{
|
||||
try {
|
||||
$validated = $request->validate([
|
||||
'plan_id' => 'required|exists:plans,id',
|
||||
]);
|
||||
|
||||
$plan = Plan::findOrFail($validated['plan_id']);
|
||||
$providers = $this->orchestrator->getActiveProvidersForPlan($plan);
|
||||
|
||||
$methods = $providers->map(function ($provider) use ($plan) {
|
||||
return [
|
||||
'provider' => $provider->getName(),
|
||||
'name' => $provider->getName(),
|
||||
'supports_recurring' => $provider->supportsRecurring(),
|
||||
'supports_one_time' => $provider->supportsOneTime(),
|
||||
'supported_currencies' => $provider->getSupportedCurrencies(),
|
||||
'fees' => $provider->calculateFees($plan->price),
|
||||
'active' => $provider->isActive(),
|
||||
];
|
||||
})->values()->toArray();
|
||||
|
||||
return response()->json([
|
||||
'success' => true,
|
||||
'data' => [
|
||||
'plan' => [
|
||||
'id' => $plan->id,
|
||||
'name' => $plan->name,
|
||||
'price' => $plan->price,
|
||||
'monthly_billing' => $plan->monthly_billing,
|
||||
],
|
||||
'payment_methods' => $methods,
|
||||
],
|
||||
]);
|
||||
|
||||
} catch (ValidationException $e) {
|
||||
return response()->json([
|
||||
'success' => false,
|
||||
'errors' => $e->errors(),
|
||||
], 422);
|
||||
} catch (\Exception $e) {
|
||||
return response()->json([
|
||||
'success' => false,
|
||||
'error' => $e->getMessage(),
|
||||
], 500);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get user's payment/subscription history
|
||||
*/
|
||||
public function getHistory(Request $request): JsonResponse
|
||||
{
|
||||
try {
|
||||
$validated = $request->validate([
|
||||
'limit' => 'nullable|integer|min:1|max:100',
|
||||
'offset' => 'nullable|integer|min:0',
|
||||
'filters' => 'nullable|array',
|
||||
]);
|
||||
|
||||
$user = $request->user();
|
||||
$limit = $validated['limit'] ?? 20;
|
||||
$filters = $validated['filters'] ?? [];
|
||||
|
||||
$history = $this->orchestrator->getTransactionHistory($user, $filters);
|
||||
|
||||
// Apply pagination
|
||||
$offset = $validated['offset'] ?? 0;
|
||||
$paginatedHistory = array_slice($history, $offset, $limit);
|
||||
|
||||
return response()->json([
|
||||
'success' => true,
|
||||
'data' => [
|
||||
'transactions' => $paginatedHistory,
|
||||
'pagination' => [
|
||||
'total' => count($history),
|
||||
'limit' => $limit,
|
||||
'offset' => $offset,
|
||||
'has_more' => $offset + $limit < count($history),
|
||||
],
|
||||
],
|
||||
]);
|
||||
|
||||
} catch (ValidationException $e) {
|
||||
return response()->json([
|
||||
'success' => false,
|
||||
'errors' => $e->errors(),
|
||||
], 422);
|
||||
} catch (\Exception $e) {
|
||||
return response()->json([
|
||||
'success' => false,
|
||||
'error' => $e->getMessage(),
|
||||
], 500);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle successful payment redirect
|
||||
*/
|
||||
public function success(Request $request): JsonResponse
|
||||
{
|
||||
return response()->json([
|
||||
'status' => 'success',
|
||||
'message' => 'Payment completed successfully',
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle cancelled payment redirect
|
||||
*/
|
||||
public function cancel(Request $request): JsonResponse
|
||||
{
|
||||
return response()->json([
|
||||
'status' => 'cancelled',
|
||||
'message' => 'Payment was cancelled',
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle payment provider webhooks
|
||||
*/
|
||||
public function webhook(Request $request, string $provider): JsonResponse
|
||||
{
|
||||
try {
|
||||
$result = $this->orchestrator->processWebhook($provider, $request);
|
||||
|
||||
return response()->json([
|
||||
'status' => 'processed',
|
||||
'result' => $result,
|
||||
]);
|
||||
} catch (\Exception $e) {
|
||||
return response()->json([
|
||||
'status' => 'error',
|
||||
'message' => $e->getMessage(),
|
||||
], 400);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user