- 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
431 lines
14 KiB
PHP
431 lines
14 KiB
PHP
<?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');
|
|
}
|
|
}
|