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:
idevakk
2025-11-19 09:37:00 -08:00
parent 0560016f33
commit 27ac13948c
83 changed files with 15613 additions and 103 deletions

View File

@@ -149,4 +149,287 @@ class User extends Authenticatable implements FilamentUser, MustVerifyEmail
{
return $this->hasMany(ImpersonationLog::class, 'target_user_id');
}
/**
* Get all subscriptions for the user
*/
public function subscriptions()
{
return $this->hasMany(Subscription::class);
}
/**
* Get the current active subscription for the user
*/
public function currentSubscription()
{
return $this->hasOne(Subscription::class)
->where(function ($query) {
$query->where('status', 'active')
->orWhere('status', 'trialing');
})
->where(function ($query) {
$query->whereNull('ends_at')
->orWhere('ends_at', '>', now());
})
->latest();
}
/**
* Get the latest subscription (regardless of status)
*/
public function latestSubscription()
{
return $this->hasOne(Subscription::class)->latestOfMany();
}
/**
* Scope: Users with active subscriptions
*/
public function scopeWithActiveSubscription($query)
{
return $query->whereHas('subscriptions', function ($subscriptionQuery) {
$subscriptionQuery->where(function ($q) {
$q->where('status', 'active')
->orWhere('status', 'trialing');
})->where(function ($q) {
$q->whereNull('ends_at')
->orWhere('ends_at', '>', now());
});
});
}
/**
* Scope: Users with trial subscriptions
*/
public function scopeWithTrialSubscription($query)
{
return $query->whereHas('subscriptions', function ($subscriptionQuery) {
$subscriptionQuery->where('status', 'trialing')
->where('trial_ends_at', '>', now());
});
}
/**
* Scope: Users with cancelled subscriptions
*/
public function scopeWithCancelledSubscription($query)
{
return $query->whereHas('subscriptions', function ($subscriptionQuery) {
$subscriptionQuery->where('status', 'cancelled')
->orWhere(function ($q) {
$q->where('ends_at', '<=', now());
});
});
}
/**
* Scope: Users without any active subscriptions
*/
public function scopeWithoutActiveSubscription($query)
{
return $query->whereDoesntHave('subscriptions', function ($subscriptionQuery) {
$subscriptionQuery->where(function ($q) {
$q->where('status', 'active')
->orWhere('status', 'trialing');
})->where(function ($q) {
$q->whereNull('ends_at')
->orWhere('ends_at', '>', now());
});
});
}
/**
* Scope: Users by subscription provider
*/
public function scopeBySubscriptionProvider($query, string $provider)
{
return $query->whereHas('subscriptions', function ($subscriptionQuery) use ($provider) {
$subscriptionQuery->where('provider', $provider)
->where(function ($q) {
$q->whereNull('ends_at')
->orWhere('ends_at', '>', now());
});
});
}
/**
* Scope: Users with subscriptions expiring soon (within given days)
*/
public function scopeWithSubscriptionExpiringSoon($query, int $days = 7)
{
return $query->whereHas('subscriptions', function ($subscriptionQuery) use ($days) {
$subscriptionQuery->where('status', 'active')
->whereNotNull('ends_at')
->where('ends_at', '<=', now()->addDays($days))
->where('ends_at', '>', now());
});
}
/**
* Check if user has an active subscription
*/
public function hasActiveSubscription(): bool
{
return $this->subscriptions()
->where(function ($query) {
$query->where('status', 'active')
->orWhere('status', 'trialing');
})
->where(function ($query) {
$query->whereNull('ends_at')
->orWhere('ends_at', '>', now());
})
->exists();
}
/**
* Check if user is currently on trial
*/
public function isOnTrial(): bool
{
return $this->subscriptions()
->where('status', 'trialing')
->where('trial_ends_at', '>', now())
->exists();
}
/**
* Check if user has cancelled subscription
*/
public function hasCancelledSubscription(): bool
{
return $this->subscriptions()
->where(function ($query) {
$query->where('status', 'cancelled')
->orWhere(function ($q) {
$q->whereNotNull('ends_at')
->where('ends_at', '<=', now());
});
})
->exists();
}
/**
* Check if user has ever had a subscription
*/
public function hasHadSubscription(): bool
{
return $this->subscriptions()->exists();
}
/**
* Get user's subscription status as string
*/
public function getSubscriptionStatus(): string
{
if ($this->isOnTrial()) {
return 'trialing';
}
if ($this->hasActiveSubscription()) {
return 'active';
}
if ($this->hasCancelledSubscription()) {
return 'cancelled';
}
return 'none';
}
/**
* Get user's current subscription plan
*/
public function getCurrentPlan(): ?Plan
{
return $this->currentSubscription?->plan;
}
/**
* Get user's subscription expiry date
*/
public function getSubscriptionExpiryDate(): ?\Carbon\Carbon
{
return $this->currentSubscription?->ends_at;
}
/**
* Get user's trial end date
*/
public function getTrialEndDate(): ?\Carbon\Carbon
{
$trialSubscription = $this->subscriptions()
->where('status', 'trialing')
->where('trial_ends_at', '>', now())
->first();
return $trialSubscription?->trial_ends_at;
}
/**
* Check if user's subscription is expiring soon (within given days)
*/
public function isSubscriptionExpiringSoon(int $days = 7): bool
{
$currentSubscription = $this->currentSubscription;
return $currentSubscription &&
$currentSubscription->ends_at &&
$currentSubscription->ends_at->lte(now()->addDays($days)) &&
$currentSubscription->ends_at->gt(now());
}
/**
* Get total amount spent by user across all subscriptions
*/
public function getTotalSpent(): float
{
return $this->subscriptions()
->with('plan')
->get()
->sum(function ($subscription) {
return $subscription->plan ? $subscription->plan->price : 0;
});
}
/**
* Get user's subscription provider
*/
public function getSubscriptionProvider(): ?string
{
return $this->currentSubscription?->provider;
}
/**
* Check if user can upgrade/downgrade their plan
*/
public function canChangePlan(): bool
{
return $this->hasActiveSubscription() &&
$this->currentSubscription?->isRecurring();
}
/**
* Get subscription metrics for analytics
*/
public function getSubscriptionMetrics(): array
{
$subscriptions = $this->subscriptions()->with('plan')->get();
return [
'total_subscriptions' => $subscriptions->count(),
'active_subscriptions' => $subscriptions->where(function ($sub) {
return in_array($sub->status, ['active', 'trialing']) &&
(!$sub->ends_at || $sub->ends_at->isFuture());
})->count(),
'total_spent' => $this->getTotalSpent(),
'current_plan' => $this->getCurrentPlan()?->name,
'provider' => $this->getSubscriptionProvider(),
'status' => $this->getSubscriptionStatus(),
'trial_ends_at' => $this->getTrialEndDate(),
'subscription_ends_at' => $this->getSubscriptionExpiryDate(),
'is_expiring_soon' => $this->isSubscriptionExpiringSoon(),
];
}
}