- 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
302 lines
7.4 KiB
PHP
302 lines
7.4 KiB
PHP
<?php
|
|
|
|
namespace App\Models;
|
|
|
|
use Illuminate\Database\Eloquent\Model;
|
|
|
|
class PaymentProvider extends Model
|
|
{
|
|
protected $fillable = [
|
|
'name',
|
|
'display_name',
|
|
'description',
|
|
'is_active',
|
|
'configuration',
|
|
'supports_recurring',
|
|
'supports_one_time',
|
|
'supported_currencies',
|
|
'webhook_url',
|
|
'webhook_secret',
|
|
'fee_structure',
|
|
'priority',
|
|
'is_fallback',
|
|
];
|
|
|
|
protected $casts = [
|
|
'is_active' => 'boolean',
|
|
'configuration' => 'array',
|
|
'supports_recurring' => 'boolean',
|
|
'supports_one_time' => 'boolean',
|
|
'supported_currencies' => 'array',
|
|
'fee_structure' => 'array',
|
|
'is_fallback' => 'boolean',
|
|
];
|
|
|
|
/**
|
|
* Scope to get only active providers
|
|
*/
|
|
public function scopeActive($query)
|
|
{
|
|
return $query->where('is_active', true);
|
|
}
|
|
|
|
/**
|
|
* Scope to get providers that support recurring payments
|
|
*/
|
|
public function scopeRecurring($query)
|
|
{
|
|
return $query->where('supports_recurring', true);
|
|
}
|
|
|
|
/**
|
|
* Scope to get providers that support one-time payments
|
|
*/
|
|
public function scopeOneTime($query)
|
|
{
|
|
return $query->where('supports_one_time', true);
|
|
}
|
|
|
|
/**
|
|
* Scope to get providers ordered by priority
|
|
*/
|
|
public function scopeByPriority($query)
|
|
{
|
|
return $query->orderBy('priority', 'desc');
|
|
}
|
|
|
|
/**
|
|
* Get the fallback provider
|
|
*/
|
|
public static function getFallback()
|
|
{
|
|
return static::where('is_fallback', true)->active()->first();
|
|
}
|
|
|
|
/**
|
|
* Check if provider supports a specific currency
|
|
*/
|
|
public function supportsCurrency(string $currency): bool
|
|
{
|
|
return in_array(strtoupper($currency), $this->supported_currencies);
|
|
}
|
|
|
|
/**
|
|
* Get fee for a specific amount
|
|
*/
|
|
public function calculateFee(float $amount): array
|
|
{
|
|
if (! $this->fee_structure) {
|
|
return [
|
|
'fixed_fee' => 0,
|
|
'percentage_fee' => 0,
|
|
'total_fee' => 0,
|
|
'net_amount' => $amount,
|
|
];
|
|
}
|
|
|
|
$fixedFee = $this->fee_structure['fixed_fee'] ?? 0;
|
|
$percentageFee = $this->fee_structure['percentage_fee'] ?? 0;
|
|
$percentageAmount = ($amount * $percentageFee) / 100;
|
|
$totalFee = $fixedFee + $percentageAmount;
|
|
|
|
return [
|
|
'fixed_fee' => $fixedFee,
|
|
'percentage_fee' => $percentageAmount,
|
|
'total_fee' => $totalFee,
|
|
'net_amount' => $amount - $totalFee,
|
|
];
|
|
}
|
|
|
|
/**
|
|
* Get webhook endpoint URL
|
|
*/
|
|
public function getWebhookUrl(): string
|
|
{
|
|
return $this->webhook_url ?? route('webhook.payment', $this->name);
|
|
}
|
|
|
|
/**
|
|
* Update configuration
|
|
*/
|
|
public function updateConfiguration(array $config): void
|
|
{
|
|
$this->configuration = array_merge($this->configuration ?? [], $config);
|
|
$this->save();
|
|
}
|
|
|
|
/**
|
|
* Get specific configuration value
|
|
*/
|
|
public function getConfigValue(string $key, $default = null)
|
|
{
|
|
return data_get($this->configuration, $key, $default);
|
|
}
|
|
|
|
/**
|
|
* Set specific configuration value
|
|
*/
|
|
public function setConfigValue(string $key, $value): void
|
|
{
|
|
$config = $this->configuration ?? [];
|
|
data_set($config, $key, $value);
|
|
$this->configuration = $config;
|
|
$this->save();
|
|
}
|
|
|
|
/**
|
|
* Check if provider is properly configured
|
|
*/
|
|
public function isConfigured(): bool
|
|
{
|
|
$requiredFields = $this->getConfigValue('required_fields', []);
|
|
|
|
foreach ($requiredFields as $field) {
|
|
if (empty($this->getConfigValue($field))) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Get provider statistics
|
|
*/
|
|
public function getStats(): array
|
|
{
|
|
$subscriptionCount = Subscription::where('provider', $this->name)->count();
|
|
$activeSubscriptionCount = Subscription::where('provider', $this->name)
|
|
->where('unified_status', 'active')
|
|
->count();
|
|
|
|
return [
|
|
'name' => $this->name,
|
|
'display_name' => $this->display_name,
|
|
'is_active' => $this->is_active,
|
|
'is_configured' => $this->isConfigured(),
|
|
'total_subscriptions' => $subscriptionCount,
|
|
'active_subscriptions' => $activeSubscriptionCount,
|
|
'supports_recurring' => $this->supports_recurring,
|
|
'supports_one_time' => $this->supports_one_time,
|
|
'supported_currencies' => $this->supported_currencies,
|
|
'is_fallback' => $this->is_fallback,
|
|
'priority' => $this->priority,
|
|
];
|
|
}
|
|
|
|
/**
|
|
* Activate provider
|
|
*/
|
|
public function activate(): void
|
|
{
|
|
$this->is_active = true;
|
|
$this->save();
|
|
}
|
|
|
|
/**
|
|
* Deactivate provider
|
|
*/
|
|
public function deactivate(): void
|
|
{
|
|
if ($this->is_fallback) {
|
|
throw new \Exception('Cannot deactivate fallback provider');
|
|
}
|
|
|
|
$this->is_active = false;
|
|
$this->save();
|
|
}
|
|
|
|
/**
|
|
* Set as fallback provider
|
|
*/
|
|
public function setAsFallback(): void
|
|
{
|
|
// Remove fallback status from other providers
|
|
static::where('is_fallback', true)->update(['is_fallback' => false]);
|
|
|
|
$this->is_fallback = true;
|
|
$this->save();
|
|
}
|
|
|
|
/**
|
|
* Remove fallback status
|
|
*/
|
|
public function removeFallback(): void
|
|
{
|
|
$this->is_fallback = false;
|
|
$this->save();
|
|
}
|
|
|
|
/**
|
|
* Get provider class name
|
|
*/
|
|
public function getProviderClass(): string
|
|
{
|
|
return $this->getConfigValue('class', '');
|
|
}
|
|
|
|
/**
|
|
* Test provider connection
|
|
*/
|
|
public function testConnection(): array
|
|
{
|
|
try {
|
|
$class = $this->getProviderClass();
|
|
|
|
if (! class_exists($class)) {
|
|
return [
|
|
'success' => false,
|
|
'error' => "Provider class {$class} not found",
|
|
];
|
|
}
|
|
|
|
$provider = new $class($this->configuration);
|
|
|
|
if (! $provider instanceof \App\Contracts\Payments\PaymentProviderContract) {
|
|
return [
|
|
'success' => false,
|
|
'error' => 'Provider class does not implement PaymentProviderContract',
|
|
];
|
|
}
|
|
|
|
$isActive = $provider->isActive();
|
|
|
|
return [
|
|
'success' => true,
|
|
'is_active' => $isActive,
|
|
'configuration_valid' => $this->isConfigured(),
|
|
];
|
|
|
|
} catch (\Exception $e) {
|
|
return [
|
|
'success' => false,
|
|
'error' => $e->getMessage(),
|
|
];
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Get all active providers ordered by priority
|
|
*/
|
|
public static function getActiveOrdered()
|
|
{
|
|
return static::active()->byPriority()->get();
|
|
}
|
|
|
|
/**
|
|
* Get providers that support a specific plan type
|
|
*/
|
|
public static function getForPlanType(bool $recurring = false)
|
|
{
|
|
$query = static::active();
|
|
|
|
if ($recurring) {
|
|
$query->recurring();
|
|
} else {
|
|
$query->oneTime();
|
|
}
|
|
|
|
return $query->byPriority()->get();
|
|
}
|
|
}
|