'decimal:2', 'min_amount' => 'decimal:2', 'max_discount' => 'decimal:2', 'package_ids' => 'array', 'max_uses' => 'integer', 'max_uses_per_workspace' => 'integer', 'used_count' => 'integer', 'duration_months' => 'integer', 'valid_from' => 'datetime', 'valid_until' => 'datetime', 'is_active' => 'boolean', ]; // Relationships public function usages(): HasMany { return $this->hasMany(CouponUsage::class); } // Type helpers public function isPercentage(): bool { return $this->type === 'percentage'; } public function isFixedAmount(): bool { return $this->type === 'fixed_amount'; } // Duration helpers public function isOnce(): bool { return $this->duration === 'once'; } public function isRepeating(): bool { return $this->duration === 'repeating'; } public function isForever(): bool { return $this->duration === 'forever'; } // Validation public function isValid(): bool { if (! $this->is_active) { return false; } if ($this->valid_from && $this->valid_from->isFuture()) { return false; } if ($this->valid_until && $this->valid_until->isPast()) { return false; } if ($this->max_uses && $this->used_count >= $this->max_uses) { return false; } return true; } public function canBeUsedByWorkspace(int $workspaceId): bool { if (! $this->isValid()) { return false; } $workspaceUsageCount = $this->usages() ->where('workspace_id', $workspaceId) ->count(); return $workspaceUsageCount < $this->max_uses_per_workspace; } /** * Check if an Orderable entity can use this coupon. * * Uses the order's orderable relationship to check usage limits. */ public function canBeUsedByOrderable(Orderable&Model $orderable): bool { if (! $this->isValid()) { return false; } // Check usage via orders linked to this orderable $usageCount = $this->usages() ->whereHas('order', function ($query) use ($orderable) { $query->where('orderable_type', get_class($orderable)) ->where('orderable_id', $orderable->id); }) ->count(); return $usageCount < $this->max_uses_per_workspace; } /** * Check if coupon has reached its maximum usage limit. */ public function hasReachedMaxUses(): bool { if ($this->max_uses === null) { return false; } return $this->used_count >= $this->max_uses; } /** * Check if coupon is restricted to a specific package. * * Returns true if the package is in the allowed list. * Returns false if no restrictions (applies to all) or package not in list. */ public function isRestrictedToPackage(string $packageCode): bool { if (empty($this->package_ids)) { return false; } return in_array($packageCode, $this->package_ids); } public function appliesToPackage(int $packageId): bool { if ($this->applies_to === 'all') { return true; } if ($this->applies_to !== 'packages') { return false; } return in_array($packageId, $this->package_ids ?? []); } // Calculation public function calculateDiscount(float $amount): float { if ($this->min_amount && $amount < $this->min_amount) { return 0; } if ($this->isPercentage()) { $discount = $amount * ($this->value / 100); } else { $discount = $this->value; } // Cap at max_discount if set if ($this->max_discount && $discount > $this->max_discount) { $discount = $this->max_discount; } // Cap at order amount return min($discount, $amount); } // Actions public function incrementUsage(): void { $this->increment('used_count'); } // Scopes public function scopeActive($query) { return $query->where('is_active', true); } public function scopeValid($query) { return $query->active() ->where(function ($q) { $q->whereNull('valid_from') ->orWhere('valid_from', '<=', now()); }) ->where(function ($q) { $q->whereNull('valid_until') ->orWhere('valid_until', '>=', now()); }) ->where(function ($q) { $q->whereNull('max_uses') ->orWhereRaw('used_count < max_uses'); }); } public function scopeByCode($query, string $code) { return $query->where('code', strtoupper($code)); } public function getActivitylogOptions(): LogOptions { return LogOptions::defaults() ->logOnly(['code', 'name', 'is_active', 'value', 'type']) ->logOnlyDirty() ->dontSubmitEmptyLogs() ->setDescriptionForEvent(fn (string $eventName) => "Coupon {$eventName}"); } }