Files
zemailnator/app/Models/Coupon.php
idevakk 27ac13948c 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
2025-11-19 09:37:00 -08:00

256 lines
6.1 KiB
PHP

<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Model;
class Coupon extends Model
{
protected $fillable = [
'code',
'name',
'description',
'type',
'value',
'minimum_amount',
'max_uses',
'uses_count',
'max_uses_per_user',
'starts_at',
'expires_at',
'is_active',
'metadata',
];
protected $casts = [
'minimum_amount' => 'decimal:2',
'value' => 'decimal:2',
'starts_at' => 'datetime',
'expires_at' => 'datetime',
'is_active' => 'boolean',
'metadata' => 'array',
];
protected $dates = [
'starts_at',
'expires_at',
];
/**
* Relationships
*/
public function usages()
{
return $this->hasMany(CouponUsage::class);
}
/**
* Check if coupon is currently valid
*/
public function isValid(?User $user = null): bool
{
// Check if coupon is active
if (! $this->is_active) {
return false;
}
// Check start date
if ($this->starts_at && $this->starts_at->isFuture()) {
return false;
}
// Check expiration date
if ($this->expires_at && $this->expires_at->isPast()) {
return false;
}
// Check usage limits
if ($this->max_uses && $this->uses_count >= $this->max_uses) {
return false;
}
// Check per-user usage limits
if ($user && $this->max_uses_per_user) {
$userUsageCount = $this->usages()->where('user_id', $user->id)->count();
if ($userUsageCount >= $this->max_uses_per_user) {
return false;
}
}
return true;
}
/**
* Calculate discount amount for a given subtotal
*/
public function calculateDiscount(float $subtotal): float
{
// Check minimum amount requirement
if ($this->minimum_amount && $subtotal < $this->minimum_amount) {
return 0;
}
if ($this->type === 'percentage') {
return $subtotal * ($this->value / 100);
}
return min($this->value, $subtotal);
}
/**
* Apply coupon to a subscription
*/
public function applyToSubscription(Subscription $subscription, float $amount, string $currency = 'USD'): CouponUsage
{
$discountAmount = $this->calculateDiscount($amount);
$usage = $this->usages()->create([
'user_id' => $subscription->user_id,
'subscription_id' => $subscription->id,
'discount_amount' => $discountAmount,
'currency' => $currency,
'used_at' => now(),
]);
// Increment usage count
$this->increment('uses_count');
return $usage;
}
/**
* Get formatted discount value
*/
public function getFormattedDiscountAttribute(): string
{
if ($this->type === 'percentage') {
return $this->value.'%';
}
return '$'.number_format($this->value, 2);
}
/**
* Get remaining uses
*/
public function getRemainingUsesAttribute(): ?int
{
if (! $this->max_uses) {
return null;
}
return max(0, $this->max_uses - $this->uses_count);
}
/**
* Get remaining uses for specific user
*/
public function getRemainingUsesForUser(User $user): ?int
{
if (! $this->max_uses_per_user) {
return null;
}
$userUsageCount = $this->usages()->where('user_id', $user->id)->count();
return max(0, $this->max_uses_per_user - $userUsageCount);
}
/**
* Check if coupon is expiring soon (within 7 days)
*/
public function isExpiringSoon(): bool
{
return $this->expires_at &&
$this->expires_at->copy()->subDays(7)->isPast() &&
$this->expires_at->isFuture();
}
/**
* Scope: Active coupons
*/
public function scopeActive(Builder $query): Builder
{
return $query->where('is_active', true);
}
/**
* Scope: Valid for use right now
*/
public function scopeValid(Builder $query, ?User $user = null): Builder
{
$query->where('is_active', true)
->where(function ($q) {
$q->whereNull('starts_at')
->orWhere('starts_at', '<=', now());
})
->where(function ($q) {
$q->whereNull('expires_at')
->orWhere('expires_at', '>', now());
});
// Check global usage limits
$query->where(function ($q) {
$q->whereNull('max_uses')
->orWhereRaw('uses_count < max_uses');
});
return $query;
}
/**
* Scope: By type
*/
public function scopeByType(Builder $query, string $type): Builder
{
return $query->where('type', $type);
}
/**
* Scope: Expiring soon
*/
public function scopeExpiringSoon(Builder $query, int $days = 7): Builder
{
return $query->where('expires_at', '<=', now()->addDays($days))
->where('expires_at', '>', now());
}
/**
* Scope: Search by code or name
*/
public function scopeSearch(Builder $query, string $term): Builder
{
return $query->where(function ($q) use ($term) {
$q->where('code', 'like', "%{$term}%")
->orWhere('name', 'like', "%{$term}%");
});
}
/**
* Find coupon by code
*/
public static function findByCode(string $code): ?self
{
return static::where('code', strtoupper($code))->first();
}
/**
* Boot: Automatically uppercase coupon codes
*/
protected static function boot()
{
parent::boot();
static::creating(function ($coupon) {
$coupon->code = strtoupper($coupon->code);
});
static::updating(function ($coupon) {
if ($coupon->isDirty('code')) {
$coupon->code = strtoupper($coupon->code);
}
});
}
}