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:
362
app/Services/Payments/ProviderRegistry.php
Normal file
362
app/Services/Payments/ProviderRegistry.php
Normal file
@@ -0,0 +1,362 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services\Payments;
|
||||
|
||||
use App\Contracts\Payments\PaymentProviderContract;
|
||||
use Illuminate\Support\Collection;
|
||||
use Illuminate\Support\Facades\Cache;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
|
||||
class ProviderRegistry
|
||||
{
|
||||
protected array $providers = [];
|
||||
|
||||
protected array $configurations = [];
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->loadConfigurations();
|
||||
$this->registerDefaultProviders();
|
||||
}
|
||||
|
||||
/**
|
||||
* Register a payment provider
|
||||
*/
|
||||
public function register(string $name, PaymentProviderContract $provider): void
|
||||
{
|
||||
$this->providers[$name] = $provider;
|
||||
|
||||
Log::info('Payment provider registered', [
|
||||
'provider' => $name,
|
||||
'class' => get_class($provider),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a specific provider
|
||||
*/
|
||||
public function get(string $name): ?PaymentProviderContract
|
||||
{
|
||||
return $this->providers[$name] ?? null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all registered providers
|
||||
*/
|
||||
public function getAllProviders(): Collection
|
||||
{
|
||||
return collect($this->providers);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get only active providers
|
||||
*/
|
||||
public function getActiveProviders(): Collection
|
||||
{
|
||||
return collect($this->providers)
|
||||
->filter(fn ($provider) => $provider->isActive());
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if provider exists
|
||||
*/
|
||||
public function has(string $name): bool
|
||||
{
|
||||
return isset($this->providers[$name]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Unregister a provider
|
||||
*/
|
||||
public function unregister(string $name): bool
|
||||
{
|
||||
if (isset($this->providers[$name])) {
|
||||
unset($this->providers[$name]);
|
||||
Log::info('Payment provider unregistered', ['provider' => $name]);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get provider configuration
|
||||
*/
|
||||
public function getConfiguration(string $providerName): array
|
||||
{
|
||||
return $this->configurations[$providerName] ?? [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Update provider configuration
|
||||
*/
|
||||
public function updateConfiguration(string $providerName, array $config): void
|
||||
{
|
||||
$this->configurations[$providerName] = $config;
|
||||
|
||||
Cache::put("payment_config_{$providerName}", $config);
|
||||
|
||||
Log::info('Payment provider configuration updated', [
|
||||
'provider' => $providerName,
|
||||
'config_keys' => array_keys($config),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get providers that support recurring payments
|
||||
*/
|
||||
public function getRecurringProviders(): Collection
|
||||
{
|
||||
return $this->getActiveProviders()
|
||||
->filter(fn ($provider) => $provider->supportsRecurring());
|
||||
}
|
||||
|
||||
/**
|
||||
* Get providers that support one-time payments
|
||||
*/
|
||||
public function getOneTimeProviders(): Collection
|
||||
{
|
||||
return $this->getActiveProviders()
|
||||
->filter(fn ($provider) => $provider->supportsOneTime());
|
||||
}
|
||||
|
||||
/**
|
||||
* Get providers that support a specific currency
|
||||
*/
|
||||
public function getProvidersForCurrency(string $currency): Collection
|
||||
{
|
||||
return $this->getActiveProviders()
|
||||
->filter(function ($provider) use ($currency) {
|
||||
return in_array($currency, $provider->getSupportedCurrencies());
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get provider by webhook URL pattern
|
||||
*/
|
||||
public function getProviderByWebhookUrl(string $url): ?PaymentProviderContract
|
||||
{
|
||||
return $this->getActiveProviders()
|
||||
->first(function ($provider) use ($url) {
|
||||
$config = $provider->getConfiguration();
|
||||
$webhookUrl = $config['webhook_url'] ?? null;
|
||||
|
||||
return $webhookUrl && str_contains($url, parse_url($webhookUrl, PHP_URL_PATH));
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate provider health status
|
||||
*/
|
||||
public function validateProviders(): array
|
||||
{
|
||||
$results = [];
|
||||
|
||||
foreach ($this->providers as $name => $provider) {
|
||||
try {
|
||||
$isActive = $provider->isActive();
|
||||
$config = $provider->getConfiguration();
|
||||
|
||||
$results[$name] = [
|
||||
'active' => $isActive,
|
||||
'configured' => ! empty($config),
|
||||
'supports_recurring' => $provider->supportsRecurring(),
|
||||
'supports_one_time' => $provider->supportsOneTime(),
|
||||
'supported_currencies' => $provider->getSupportedCurrencies(),
|
||||
'last_checked' => now()->toISOString(),
|
||||
];
|
||||
|
||||
if (! $isActive) {
|
||||
Log::warning('Payment provider is inactive', ['provider' => $name]);
|
||||
}
|
||||
|
||||
} catch (\Exception $e) {
|
||||
$results[$name] = [
|
||||
'active' => false,
|
||||
'error' => $e->getMessage(),
|
||||
'last_checked' => now()->toISOString(),
|
||||
];
|
||||
|
||||
Log::error('Payment provider health check failed', [
|
||||
'provider' => $name,
|
||||
'error' => $e->getMessage(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
return $results;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get provider statistics
|
||||
*/
|
||||
public function getProviderStats(): array
|
||||
{
|
||||
$stats = [
|
||||
'total_providers' => count($this->providers),
|
||||
'active_providers' => 0,
|
||||
'recurring_providers' => 0,
|
||||
'one_time_providers' => 0,
|
||||
'supported_currencies' => [],
|
||||
'providers' => [],
|
||||
];
|
||||
|
||||
foreach ($this->providers as $name => $provider) {
|
||||
$isActive = $provider->isActive();
|
||||
|
||||
if ($isActive) {
|
||||
$stats['active_providers']++;
|
||||
}
|
||||
|
||||
if ($provider->supportsRecurring()) {
|
||||
$stats['recurring_providers']++;
|
||||
}
|
||||
|
||||
if ($provider->supportsOneTime()) {
|
||||
$stats['one_time_providers']++;
|
||||
}
|
||||
|
||||
$stats['supported_currencies'] = array_merge(
|
||||
$stats['supported_currencies'],
|
||||
$provider->getSupportedCurrencies()
|
||||
);
|
||||
|
||||
$stats['providers'][$name] = [
|
||||
'active' => $isActive,
|
||||
'class' => get_class($provider),
|
||||
'supports_recurring' => $provider->supportsRecurring(),
|
||||
'supports_one_time' => $provider->supportsOneTime(),
|
||||
'currencies' => $provider->getSupportedCurrencies(),
|
||||
];
|
||||
}
|
||||
|
||||
$stats['supported_currencies'] = array_unique($stats['supported_currencies']);
|
||||
|
||||
return $stats;
|
||||
}
|
||||
|
||||
/**
|
||||
* Load provider configurations from cache/database
|
||||
*/
|
||||
protected function loadConfigurations(): void
|
||||
{
|
||||
// Load from cache first
|
||||
$cachedConfigs = Cache::get('payment_providers_config', []);
|
||||
|
||||
if (empty($cachedConfigs)) {
|
||||
// Load from database or config files
|
||||
$this->configurations = config('payment.providers', []);
|
||||
|
||||
// Cache for 1 hour
|
||||
Cache::put('payment_providers_config', $this->configurations, 3600);
|
||||
} else {
|
||||
$this->configurations = $cachedConfigs;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Register default providers
|
||||
*/
|
||||
protected function registerDefaultProviders(): void
|
||||
{
|
||||
// Auto-register providers based on configuration
|
||||
$enabledProviders = config('payment.enabled_providers', []);
|
||||
|
||||
foreach ($enabledProviders as $providerName) {
|
||||
$this->registerProviderByName($providerName);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Register provider by name using configuration
|
||||
*/
|
||||
protected function registerProviderByName(string $providerName): void
|
||||
{
|
||||
$providerClass = config("payment.providers.{$providerName}.class");
|
||||
|
||||
if (! $providerClass || ! class_exists($providerClass)) {
|
||||
Log::error('Payment provider class not found', [
|
||||
'provider' => $providerName,
|
||||
'class' => $providerClass,
|
||||
]);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
$config = $this->getConfiguration($providerName);
|
||||
$provider = new $providerClass($config);
|
||||
|
||||
if ($provider instanceof PaymentProviderContract) {
|
||||
$this->register($providerName, $provider);
|
||||
} else {
|
||||
Log::error('Payment provider does not implement contract', [
|
||||
'provider' => $providerName,
|
||||
'class' => $providerClass,
|
||||
]);
|
||||
}
|
||||
|
||||
} catch (\Exception $e) {
|
||||
Log::error('Failed to register payment provider', [
|
||||
'provider' => $providerName,
|
||||
'error' => $e->getMessage(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Refresh provider (useful for configuration changes)
|
||||
*/
|
||||
public function refreshProvider(string $name): bool
|
||||
{
|
||||
if (! isset($this->providers[$name])) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Unregister current instance
|
||||
unset($this->providers[$name]);
|
||||
|
||||
// Re-register with fresh configuration
|
||||
$this->registerProviderByName($name);
|
||||
|
||||
return isset($this->providers[$name]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Enable/disable a provider
|
||||
*/
|
||||
public function toggleProvider(string $name, bool $enabled): bool
|
||||
{
|
||||
$config = $this->getConfiguration($name);
|
||||
|
||||
if (empty($config)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$config['enabled'] = $enabled;
|
||||
$this->updateConfiguration($name, $config);
|
||||
|
||||
// Refresh the provider to apply changes
|
||||
return $this->refreshProvider($name);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get provider for fallback
|
||||
*/
|
||||
public function getFallbackProvider(): ?PaymentProviderContract
|
||||
{
|
||||
$fallbackProvider = config('payment.fallback_provider');
|
||||
|
||||
if ($fallbackProvider && $this->has($fallbackProvider)) {
|
||||
$provider = $this->get($fallbackProvider);
|
||||
|
||||
if ($provider && $provider->isActive()) {
|
||||
return $provider;
|
||||
}
|
||||
}
|
||||
|
||||
// Return first active provider as fallback
|
||||
return $this->getActiveProviders()->first();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user