'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); } }); } }