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:
@@ -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(),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user