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:
301
app/Models/PaymentProvider.php
Normal file
301
app/Models/PaymentProvider.php
Normal file
@@ -0,0 +1,301 @@
|
||||
<?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();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user