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:
idevakk
2025-11-19 09:37:00 -08:00
parent 0560016f33
commit 27ac13948c
83 changed files with 15613 additions and 103 deletions

View File

@@ -0,0 +1,430 @@
<?php
namespace App\Services\Payments;
use App\Services\Payments\Providers\ActivationKeyProvider;
use App\Services\Payments\Providers\CryptoProvider;
use App\Services\Payments\Providers\LemonSqueezyProvider;
use App\Services\Payments\Providers\OxapayProvider;
use App\Services\Payments\Providers\PolarProvider;
use App\Services\Payments\Providers\StripeProvider;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\Log;
class PaymentConfigurationManager
{
protected ProviderRegistry $registry;
protected array $providerConfigs;
public function __construct(ProviderRegistry $registry)
{
$this->registry = $registry;
$this->providerConfigs = $this->loadProviderConfigurations();
}
/**
* Initialize all configured payment providers
*/
public function initializeProviders(): void
{
try {
$this->registerStripeProvider();
$this->registerLemonSqueezyProvider();
$this->registerPolarProvider();
$this->registerOxapayProvider();
$this->registerCryptoProvider();
$this->registerActivationKeyProvider();
Log::info('Payment providers initialized', [
'providers' => array_keys($this->registry->getAllProviders()->toArray()),
'active_providers' => array_keys($this->registry->getActiveProviders()->toArray()),
]);
} catch (\Exception $e) {
Log::error('Failed to initialize payment providers', [
'error' => $e->getMessage(),
]);
throw $e;
}
}
/**
* Register Stripe provider if configured
*/
protected function registerStripeProvider(): void
{
$config = $this->providerConfigs['stripe'] ?? [];
if (! empty($config['secret_key'])) {
$provider = new StripeProvider($config);
$this->registry->register('stripe', $provider);
}
}
/**
* Register Lemon Squeezy provider if configured
*/
protected function registerLemonSqueezyProvider(): void
{
$config = $this->providerConfigs['lemon_squeezy'] ?? [];
if (! empty($config['api_key']) && ! empty($config['store_id'])) {
$provider = new LemonSqueezyProvider($config);
$this->registry->register('lemon_squeezy', $provider);
}
}
/**
* Register Polar provider if configured
*/
protected function registerPolarProvider(): void
{
$config = $this->providerConfigs['polar'] ?? [];
if (! empty($config['api_key'])) {
$provider = new PolarProvider($config);
$this->registry->register('polar', $provider);
}
}
/**
* Register OxaPay provider if configured
*/
protected function registerOxapayProvider(): void
{
$config = $this->providerConfigs['oxapay'] ?? [];
if (! empty($config['merchant_api_key'])) {
$provider = new OxapayProvider($config);
$this->registry->register('oxapay', $provider);
}
}
/**
* Register Crypto provider if enabled
*/
protected function registerCryptoProvider(): void
{
$config = $this->providerConfigs['crypto'] ?? [];
if ($config['enabled'] ?? false) {
$provider = new CryptoProvider($config);
$this->registry->register('crypto', $provider);
}
}
/**
* Register Activation Key provider (always available)
*/
protected function registerActivationKeyProvider(): void
{
$config = $this->providerConfigs['activation_key'] ?? [];
$provider = new ActivationKeyProvider($config);
$this->registry->register('activation_key', $provider);
}
/**
* Load provider configurations from config and cache
*/
protected function loadProviderConfigurations(): array
{
return Cache::remember('payment_provider_configs', now()->addHour(), function () {
return [
'stripe' => [
'secret_key' => config('services.stripe.secret_key'),
'publishable_key' => config('services.stripe.publishable_key'),
'webhook_secret' => config('services.stripe.webhook_secret'),
],
'lemon_squeezy' => [
'api_key' => config('services.lemon_squeezy.api_key'),
'store_id' => config('services.lemon_squeezy.store_id'),
'webhook_secret' => config('services.lemon_squeezy.webhook_secret'),
],
'polar' => [
'api_key' => config('services.polar.api_key'),
'webhook_secret' => config('services.polar.webhook_secret'),
],
'oxapay' => [
'merchant_api_key' => config('services.oxapay.merchant_api_key'),
'webhook_url' => config('services.oxapay.webhook_url'),
'success_url' => config('services.oxapay.success_url'),
'cancel_url' => config('services.oxapay.cancel_url'),
'sandbox' => config('services.oxapay.sandbox', false),
],
'crypto' => [
'enabled' => config('payments.crypto.enabled', false),
'webhook_secret' => config('payments.crypto.webhook_secret'),
'confirmation_timeout_minutes' => config('payments.crypto.confirmation_timeout_minutes', 30),
'exchange_rate_provider' => config('payments.crypto.exchange_rate_provider', 'coingecko'),
],
'activation_key' => [
'key_prefix' => config('payments.activation_key.prefix', 'AK-'),
'key_length' => config('payments.activation_key.length', 32),
'expiration_days' => config('payments.activation_key.expiration_days'),
],
];
});
}
/**
* Get provider configuration
*/
public function getProviderConfig(string $provider): array
{
return $this->providerConfigs[$provider] ?? [];
}
/**
* Update provider configuration
*/
public function updateProviderConfig(string $provider, array $config): void
{
$this->providerConfigs[$provider] = array_merge(
$this->providerConfigs[$provider] ?? [],
$config
);
// Clear cache to force reload
Cache::forget('payment_provider_configs');
Log::info('Payment provider configuration updated', [
'provider' => $provider,
]);
}
/**
* Validate provider configuration
*/
public function validateProviderConfig(string $provider, array $config): array
{
$errors = [];
switch ($provider) {
case 'stripe':
if (empty($config['secret_key'])) {
$errors[] = 'Stripe secret key is required';
}
if (empty($config['publishable_key'])) {
$errors[] = 'Stripe publishable key is required';
}
break;
case 'lemon_squeezy':
if (empty($config['api_key'])) {
$errors[] = 'Lemon Squeezy API key is required';
}
if (empty($config['store_id'])) {
$errors[] = 'Lemon Squeezy store ID is required';
}
break;
case 'polar':
if (empty($config['api_key'])) {
$errors[] = 'Polar API key is required';
}
break;
case 'oxapay':
if (empty($config['merchant_api_key'])) {
$errors[] = 'OxaPay merchant API key is required';
}
break;
case 'crypto':
if (empty($config['webhook_secret'])) {
$errors[] = 'Crypto webhook secret is required';
}
break;
case 'activation_key':
// Activation keys don't require specific configuration
break;
default:
$errors[] = "Unknown provider: {$provider}";
}
return [
'valid' => empty($errors),
'errors' => $errors,
];
}
/**
* Get provider status and health information
*/
public function getProviderStatus(): array
{
$status = [];
$providers = $this->registry->getAllProviders();
foreach ($providers as $name => $provider) {
$status[$name] = [
'name' => $provider->getName(),
'active' => $provider->isActive(),
'supports_recurring' => $provider->supportsRecurring(),
'supports_one_time' => $provider->supportsOneTime(),
'supported_currencies' => $provider->getSupportedCurrencies(),
'configured' => ! empty($this->providerConfigs[$name]),
'configuration' => $this->sanitizeConfig($this->providerConfigs[$name] ?? []),
];
}
return $status;
}
/**
* Sanitize configuration for display (remove sensitive data)
*/
protected function sanitizeConfig(array $config): array
{
$sensitiveKeys = ['secret_key', 'api_key', 'webhook_secret'];
$sanitized = $config;
foreach ($sensitiveKeys as $key) {
if (isset($sanitized[$key]) && ! empty($sanitized[$key])) {
$sanitized[$key] = '***'.substr($sanitized[$key], -4);
}
}
return $sanitized;
}
/**
* Enable/disable a provider
*/
public function toggleProvider(string $provider, bool $enabled): bool
{
try {
if ($enabled && ! $this->registry->has($provider)) {
// Register the provider if it doesn't exist
$this->registerProviderByName($provider);
}
if (! $enabled && $this->registry->has($provider)) {
// Unregister the provider
$this->registry->unregister($provider);
}
$this->updateProviderConfig($provider, ['enabled' => $enabled]);
Log::info('Payment provider toggled', [
'provider' => $provider,
'enabled' => $enabled,
]);
return true;
} catch (\Exception $e) {
Log::error('Failed to toggle payment provider', [
'provider' => $provider,
'enabled' => $enabled,
'error' => $e->getMessage(),
]);
return false;
}
}
/**
* Register a provider by name
*/
protected function registerProviderByName(string $provider): void
{
switch ($provider) {
case 'stripe':
$this->registerStripeProvider();
break;
case 'lemon_squeezy':
$this->registerLemonSqueezyProvider();
break;
case 'polar':
$this->registerPolarProvider();
break;
case 'oxapay':
$this->registerOxapayProvider();
break;
case 'crypto':
$this->registerCryptoProvider();
break;
case 'activation_key':
$this->registerActivationKeyProvider();
break;
default:
throw new \InvalidArgumentException("Unknown provider: {$provider}");
}
}
/**
* Get default provider for a given plan type
*/
public function getDefaultProvider(?string $planType = null): string
{
// Priority order for providers
$priority = [
'stripe', // Most reliable
'lemon_squeezy', // Good for international
'polar', // Developer-focused MoR
'oxapay', // Crypto payment gateway
'crypto', // For crypto payments
'activation_key', // For manual activation
];
foreach ($priority as $provider) {
if ($this->registry->has($provider) && $this->registry->get($provider)->isActive()) {
return $provider;
}
}
// Fallback to activation key (always available)
return 'activation_key';
}
/**
* Test provider connectivity
*/
public function testProviderConnectivity(string $provider): array
{
try {
if (! $this->registry->has($provider)) {
return [
'success' => false,
'error' => 'Provider not registered',
];
}
$providerInstance = $this->registry->get($provider);
// Basic connectivity test - check if provider is active
$isActive = $providerInstance->isActive();
return [
'success' => $isActive,
'message' => $isActive ? 'Provider is active and ready' : 'Provider is not active',
'details' => [
'name' => $providerInstance->getName(),
'supports_recurring' => $providerInstance->supportsRecurring(),
'supports_one_time' => $providerInstance->supportsOneTime(),
],
];
} catch (\Exception $e) {
return [
'success' => false,
'error' => $e->getMessage(),
];
}
}
/**
* Refresh provider configurations
*/
public function refreshConfigurations(): void
{
Cache::forget('payment_provider_configs');
$this->providerConfigs = $this->loadProviderConfigurations();
Log::info('Payment provider configurations refreshed');
}
}