- Build PolarProvider from scratch with proper HTTP API integration - Add encrypted configuration loading from payment_providers table via model - Implement sandbox/live environment switching with proper credential handling - Fix product creation API structure for Polar.sh requirements - Add comprehensive error handling and logging throughout checkout flow - Fix PaymentController checkout URL handling to support Polar's checkout_url response - Add debug logging for troubleshooting checkout session creation - Support both regular and trial checkout flows for Polar payments
522 lines
16 KiB
PHP
522 lines
16 KiB
PHP
<?php
|
|
|
|
namespace App\Services\Payments;
|
|
|
|
use App\Contracts\Payments\PaymentProviderContract;
|
|
use App\Models\PaymentProvider as PaymentProviderModel;
|
|
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->loadConfigurationsFromDatabase();
|
|
$this->registerProvidersFromDatabase();
|
|
}
|
|
|
|
/**
|
|
* 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
|
|
{
|
|
try {
|
|
// Update database - encode as JSON string since the model expects to cast it
|
|
PaymentProviderModel::where('name', $providerName)
|
|
->update(['configuration' => $config]); // Laravel will automatically cast array to JSON
|
|
|
|
// Update local cache
|
|
$this->configurations[$providerName] = array_merge(
|
|
$this->configurations[$providerName] ?? [],
|
|
['config' => $config]
|
|
);
|
|
|
|
// Clear main cache
|
|
Cache::forget('payment_providers_config');
|
|
|
|
Log::info('Payment provider configuration updated', [
|
|
'provider' => $providerName,
|
|
'config_keys' => array_keys($config),
|
|
]);
|
|
} catch (\Exception $e) {
|
|
Log::error('Failed to update payment provider configuration', [
|
|
'provider' => $providerName,
|
|
'error' => $e->getMessage(),
|
|
]);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 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 database
|
|
*/
|
|
protected function loadConfigurationsFromDatabase(): void
|
|
{
|
|
try {
|
|
// Load from cache first
|
|
$cachedConfigs = Cache::get('payment_providers_config', []);
|
|
|
|
if (empty($cachedConfigs)) {
|
|
// Load from database
|
|
$providers = PaymentProviderModel::where('is_active', true)->get();
|
|
|
|
$this->configurations = [];
|
|
foreach ($providers as $provider) {
|
|
// Configuration is already cast to array by the model
|
|
$configData = $provider->configuration ?? [];
|
|
|
|
$this->configurations[$provider->name] = [
|
|
'enabled' => $provider->is_active,
|
|
'display_name' => $provider->display_name,
|
|
'class' => $configData['class'] ?? null,
|
|
'config' => $configData,
|
|
'supports_recurring' => $provider->supports_recurring,
|
|
'supports_one_time' => $provider->supports_one_time,
|
|
'supported_currencies' => $provider->supported_currencies ?? [],
|
|
'fee_structure' => $provider->fee_structure ?? [],
|
|
'priority' => $provider->priority,
|
|
'is_fallback' => $provider->is_fallback,
|
|
];
|
|
}
|
|
|
|
// Cache for 1 hour
|
|
Cache::put('payment_providers_config', $this->configurations, 3600);
|
|
} else {
|
|
$this->configurations = $cachedConfigs;
|
|
}
|
|
} catch (\Exception $e) {
|
|
Log::error('Failed to load payment provider configurations from database', [
|
|
'error' => $e->getMessage(),
|
|
]);
|
|
$this->configurations = [];
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Register providers from database
|
|
*/
|
|
protected function registerProvidersFromDatabase(): void
|
|
{
|
|
try {
|
|
$activeProviders = PaymentProviderModel::where('is_active', true)->get();
|
|
|
|
foreach ($activeProviders as $providerModel) {
|
|
$this->registerProviderFromModel($providerModel);
|
|
}
|
|
} catch (\Exception $e) {
|
|
Log::error('Failed to register payment providers from database', [
|
|
'error' => $e->getMessage(),
|
|
]);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Register provider from database model
|
|
*/
|
|
protected function registerProviderFromModel(PaymentProviderModel $providerModel): void
|
|
{
|
|
// Configuration is already cast to array by the model
|
|
$configData = $providerModel->configuration ?? [];
|
|
$providerClass = $configData['class'] ?? null;
|
|
|
|
if (! $providerClass || ! class_exists($providerClass)) {
|
|
Log::error('Payment provider class not found', [
|
|
'provider' => $providerModel->name,
|
|
'class' => $providerClass,
|
|
]);
|
|
|
|
return;
|
|
}
|
|
|
|
try {
|
|
// Use the full configuration data for the provider
|
|
$config = $this->getConfiguration($providerModel->name);
|
|
$provider = new $providerClass($config);
|
|
|
|
if ($provider instanceof PaymentProviderContract) {
|
|
$this->register($providerModel->name, $provider);
|
|
} else {
|
|
Log::error('Payment provider does not implement contract', [
|
|
'provider' => $providerModel->name,
|
|
'class' => $providerClass,
|
|
]);
|
|
}
|
|
|
|
} catch (\Exception $e) {
|
|
Log::error('Failed to register payment provider', [
|
|
'provider' => $providerModel->name,
|
|
'error' => $e->getMessage(),
|
|
]);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Get provider class name based on provider name from database
|
|
*/
|
|
protected function getProviderClass(string $providerName): ?string
|
|
{
|
|
$config = $this->getConfiguration($providerName);
|
|
|
|
return $config['class'] ?? null;
|
|
}
|
|
|
|
/**
|
|
* Get API key from database configuration
|
|
*/
|
|
protected function getApiKeyFromConfig(string $providerName): ?string
|
|
{
|
|
$config = $this->getConfiguration($providerName);
|
|
$configData = $config['config'] ?? [];
|
|
|
|
// Try different possible API key field names
|
|
$apiKeyFields = [
|
|
'secret_key',
|
|
'api_key',
|
|
'merchant_api_key',
|
|
'private_key',
|
|
'key',
|
|
];
|
|
|
|
foreach ($apiKeyFields as $field) {
|
|
if (isset($configData[$field]) && ! empty($configData[$field])) {
|
|
return $configData[$field];
|
|
}
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
/**
|
|
* Get webhook secret from database configuration
|
|
*/
|
|
protected function getWebhookSecretFromConfig(string $providerName): ?string
|
|
{
|
|
$config = $this->getConfiguration($providerName);
|
|
$configData = $config['config'] ?? [];
|
|
|
|
// Try different possible webhook secret field names
|
|
$webhookFields = [
|
|
'webhook_secret',
|
|
'secret',
|
|
'signing_secret',
|
|
];
|
|
|
|
foreach ($webhookFields as $field) {
|
|
if (isset($configData[$field]) && ! empty($configData[$field])) {
|
|
return $configData[$field];
|
|
}
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
/**
|
|
* Refresh provider (useful for configuration changes)
|
|
*/
|
|
public function refreshProvider(string $name): bool
|
|
{
|
|
try {
|
|
// Clear cache to force reload from database
|
|
Cache::forget('payment_providers_config');
|
|
|
|
// Unregister current instance
|
|
unset($this->providers[$name]);
|
|
|
|
// Reload configurations from database
|
|
$this->loadConfigurationsFromDatabase();
|
|
|
|
// Re-register from database
|
|
$providerModel = PaymentProviderModel::where('name', $name)
|
|
->where('is_active', true)
|
|
->first();
|
|
|
|
if ($providerModel) {
|
|
$this->registerProviderFromModel($providerModel);
|
|
}
|
|
|
|
return isset($this->providers[$name]);
|
|
} catch (\Exception $e) {
|
|
Log::error('Failed to refresh payment provider', [
|
|
'provider' => $name,
|
|
'error' => $e->getMessage(),
|
|
]);
|
|
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Enable/disable a provider
|
|
*/
|
|
public function toggleProvider(string $name, bool $enabled): bool
|
|
{
|
|
try {
|
|
// Update database
|
|
$updated = PaymentProviderModel::where('name', $name)
|
|
->update(['is_active' => $enabled]);
|
|
|
|
if (! $updated) {
|
|
return false;
|
|
}
|
|
|
|
// Clear cache to force reload
|
|
Cache::forget('payment_providers_config');
|
|
|
|
// Refresh the provider to apply changes
|
|
return $this->refreshProvider($name);
|
|
} catch (\Exception $e) {
|
|
Log::error('Failed to toggle payment provider', [
|
|
'provider' => $name,
|
|
'enabled' => $enabled,
|
|
'error' => $e->getMessage(),
|
|
]);
|
|
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Get provider for fallback
|
|
*/
|
|
public function getFallbackProvider(): ?PaymentProviderContract
|
|
{
|
|
try {
|
|
// First try to get the provider marked as fallback in database
|
|
$fallbackProvider = PaymentProviderModel::where('is_fallback', true)
|
|
->where('is_active', true)
|
|
->first();
|
|
|
|
if ($fallbackProvider && $this->has($fallbackProvider->name)) {
|
|
$provider = $this->get($fallbackProvider->name);
|
|
if ($provider && $provider->isActive()) {
|
|
return $provider;
|
|
}
|
|
}
|
|
|
|
// Fallback to environment variable
|
|
$fallbackProviderName = env('PAYMENT_FALLBACK_PROVIDER', 'stripe');
|
|
if ($this->has($fallbackProviderName)) {
|
|
$provider = $this->get($fallbackProviderName);
|
|
if ($provider && $provider->isActive()) {
|
|
return $provider;
|
|
}
|
|
}
|
|
|
|
// Return first active provider as final fallback
|
|
return $this->getActiveProviders()->first();
|
|
} catch (\Exception $e) {
|
|
Log::error('Failed to get fallback provider', [
|
|
'error' => $e->getMessage(),
|
|
]);
|
|
|
|
return $this->getActiveProviders()->first();
|
|
}
|
|
}
|
|
}
|