Passed
Push — master ( 5aa5e3...07d8b3 )
by Darko
09:19
created

Invitation::scopeValid()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 5
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 3
dl 0
loc 5
rs 10
c 0
b 0
f 0
cc 1
nc 1
nop 1
1
<?php
2
3
namespace App\Models;
4
5
use Illuminate\Database\Eloquent\Builder;
6
use Illuminate\Database\Eloquent\Model;
7
use Illuminate\Database\Eloquent\Relations\BelongsTo;
8
use Illuminate\Support\Str;
9
10
/**
11
 * App\Models\Invitation
12
 *
13
 * @property int $id
14
 * @property string $token
15
 * @property string $email
16
 * @property int $invited_by
17
 * @property \Illuminate\Support\Carbon $expires_at
18
 * @property \Illuminate\Support\Carbon|null $used_at
19
 * @property int|null $used_by
20
 * @property bool $is_active
21
 * @property array|null $metadata
22
 * @property \Illuminate\Support\Carbon|null $created_at
23
 * @property \Illuminate\Support\Carbon|null $updated_at
24
 * @property-read \App\Models\User $invitedBy
25
 * @property-read \App\Models\User|null $usedBy
26
 *
27
 * @method static \Illuminate\Database\Eloquent\Builder|\App\Models\Invitation newModelQuery()
28
 * @method static \Illuminate\Database\Eloquent\Builder|\App\Models\Invitation newQuery()
29
 * @method static \Illuminate\Database\Eloquent\Builder|\App\Models\Invitation query()
30
 * @method static \Illuminate\Database\Eloquent\Builder|\App\Models\Invitation active()
31
 * @method static \Illuminate\Database\Eloquent\Builder|\App\Models\Invitation valid()
32
 * @method static \Illuminate\Database\Eloquent\Builder|\App\Models\Invitation expired()
33
 * @method static \Illuminate\Database\Eloquent\Builder|\App\Models\Invitation unused()
34
 * @method static \Illuminate\Database\Eloquent\Builder|\App\Models\Invitation used()
35
 */
36
class Invitation extends Model
37
{
38
    public const DEFAULT_INVITES = 1;
39
40
    public const DEFAULT_INVITE_EXPIRY_DAYS = 7;
41
42
    /**
43
     * @var array
44
     */
45
    protected $fillable = [
46
        'token',
47
        'email',
48
        'invited_by',
49
        'expires_at',
50
        'used_at',
51
        'used_by',
52
        'is_active',
53
        'metadata',
54
    ];
55
56
    /**
57
     * @var array
58
     */
59
    protected $casts = [
60
        'expires_at' => 'datetime',
61
        'used_at' => 'datetime',
62
        'is_active' => 'boolean',
63
        'metadata' => 'array',
64
    ];
65
66
    /**
67
     * @var array
68
     */
69
    protected $dates = [
70
        'expires_at',
71
        'used_at',
72
        'created_at',
73
        'updated_at',
74
    ];
75
76
    /**
77
     * Get the user who created this invitation
78
     */
79
    public function invitedBy(): BelongsTo
80
    {
81
        return $this->belongsTo(User::class, 'invited_by');
82
    }
83
84
    /**
85
     * Get the user who used this invitation
86
     */
87
    public function usedBy(): BelongsTo
88
    {
89
        return $this->belongsTo(User::class, 'used_by');
90
    }
91
92
    /**
93
     * Scope to get only active invitations
94
     */
95
    public function scopeActive(Builder $query): Builder
96
    {
97
        return $query->where('is_active', true);
98
    }
99
100
    /**
101
     * Scope to get valid (active and not expired) invitations
102
     */
103
    public function scopeValid(Builder $query): Builder
104
    {
105
        return $query->active()
0 ignored issues
show
Bug Best Practice introduced by
The expression return $query->active()-...)->whereNull('used_at') could return the type Illuminate\Database\Query\Builder which is incompatible with the type-hinted return Illuminate\Database\Eloquent\Builder. Consider adding an additional type-check to rule them out.
Loading history...
106
            ->where('expires_at', '>', now())
107
            ->whereNull('used_at');
108
    }
109
110
    /**
111
     * Scope to get expired invitations
112
     */
113
    public function scopeExpired(Builder $query): Builder
114
    {
115
        return $query->where('expires_at', '<=', now());
116
    }
117
118
    /**
119
     * Scope to get unused invitations
120
     */
121
    public function scopeUnused(Builder $query): Builder
122
    {
123
        return $query->whereNull('used_at');
0 ignored issues
show
Bug Best Practice introduced by
The expression return $query->whereNull('used_at') could return the type Illuminate\Database\Query\Builder which is incompatible with the type-hinted return Illuminate\Database\Eloquent\Builder. Consider adding an additional type-check to rule them out.
Loading history...
124
    }
125
126
    /**
127
     * Scope to get used invitations
128
     */
129
    public function scopeUsed(Builder $query): Builder
130
    {
131
        return $query->whereNotNull('used_at');
0 ignored issues
show
Bug Best Practice introduced by
The expression return $query->whereNotNull('used_at') could return the type Illuminate\Database\Query\Builder which is incompatible with the type-hinted return Illuminate\Database\Eloquent\Builder. Consider adding an additional type-check to rule them out.
Loading history...
132
    }
133
134
    /**
135
     * Check if the invitation is valid
136
     */
137
    public function isValid(): bool
138
    {
139
        return $this->is_active &&
140
               $this->expires_at->isFuture() &&
141
               is_null($this->used_at);
142
    }
143
144
    /**
145
     * Check if the invitation is expired
146
     */
147
    public function isExpired(): bool
148
    {
149
        return $this->expires_at->isPast();
150
    }
151
152
    /**
153
     * Check if the invitation has been used
154
     */
155
    public function isUsed(): bool
156
    {
157
        return ! is_null($this->used_at);
158
    }
159
160
    /**
161
     * Mark the invitation as used
162
     */
163
    public function markAsUsed(int $userId): bool
164
    {
165
        $this->used_at = now();
166
        $this->used_by = $userId;
167
        $this->is_active = false;
168
169
        return $this->save();
170
    }
171
172
    /**
173
     * Mark the invitation as expired
174
     */
175
    public function markAsExpired(): bool
176
    {
177
        $this->is_active = false;
178
179
        return $this->save();
180
    }
181
182
    /**
183
     * Create a new invitation
184
     */
185
    public static function createInvitation(
186
        string $email,
187
        int $invitedBy,
188
        int $expiryDays = self::DEFAULT_INVITE_EXPIRY_DAYS,
189
        array $metadata = []
190
    ): self {
191
        return self::create([
0 ignored issues
show
Bug Best Practice introduced by
The expression return self::create(arra...etadata' => $metadata)) could return the type Illuminate\Database\Eloq...gHasThroughRelationship which is incompatible with the type-hinted return App\Models\Invitation. Consider adding an additional type-check to rule them out.
Loading history...
192
            'token' => Str::random(64),
193
            'email' => strtolower(trim($email)),
194
            'invited_by' => $invitedBy,
195
            'expires_at' => now()->addDays($expiryDays),
196
            'metadata' => $metadata,
197
        ]);
198
    }
199
200
    /**
201
     * Find invitation by token
202
     */
203
    public static function findByToken(string $token): ?self
204
    {
205
        return self::where('token', $token)->first();
206
    }
207
208
    /**
209
     * Find valid invitation by token
210
     */
211
    public static function findValidByToken(string $token): ?self
212
    {
213
        return self::valid()->where('token', $token)->first();
214
    }
215
216
    /**
217
     * Find invitation by email
218
     */
219
    public static function findByEmail(string $email): ?self
220
    {
221
        return self::where('email', strtolower(trim($email)))->first();
222
    }
223
224
    /**
225
     * Get invitation by token (legacy compatibility)
226
     */
227
    public static function getInvite(string $token): ?self
228
    {
229
        return self::findValidByToken($token);
230
    }
231
232
    /**
233
     * Delete invitation by token (legacy compatibility)
234
     */
235
    public static function deleteInvite(string $token): bool
236
    {
237
        $invitation = self::findByToken($token);
238
        if ($invitation) {
0 ignored issues
show
introduced by
$invitation is of type App\Models\Invitation, thus it always evaluated to true.
Loading history...
239
            return $invitation->delete();
0 ignored issues
show
Bug Best Practice introduced by
The expression return $invitation->delete() could return the type null which is incompatible with the type-hinted return boolean. Consider adding an additional type-check to rule them out.
Loading history...
240
        }
241
242
        return false;
243
    }
244
245
    /**
246
     * Add invite (legacy compatibility)
247
     */
248
    public static function addInvite(int $userId, string $token): self
249
    {
250
        // For legacy compatibility, we create an invitation with a specific token
251
        // In practice, this should use createInvitation method instead
252
        return self::create([
0 ignored issues
show
Bug Best Practice introduced by
The expression return self::create(arra...T_INVITE_EXPIRY_DAYS))) could return the type Illuminate\Database\Eloq...gHasThroughRelationship which is incompatible with the type-hinted return App\Models\Invitation. Consider adding an additional type-check to rule them out.
Loading history...
253
            'token' => $token,
254
            'email' => '', // Legacy method doesn't specify email
255
            'invited_by' => $userId,
256
            'expires_at' => now()->addDays(self::DEFAULT_INVITE_EXPIRY_DAYS),
257
        ]);
258
    }
259
260
    /**
261
     * Clean up expired invitations
262
     */
263
    public static function cleanupExpired(): int
264
    {
265
        $expiredCount = self::expired()->active()->count();
266
        self::expired()->active()->update(['is_active' => false]);
267
268
        return $expiredCount;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $expiredCount could return the type Illuminate\Database\Eloquent\Builder which is incompatible with the type-hinted return integer. Consider adding an additional type-check to rule them out.
Loading history...
269
    }
270
271
    /**
272
     * Get invitation statistics
273
     */
274
    public static function getStats(): array
275
    {
276
        return [
277
            'total' => self::count(),
278
            'active' => self::active()->count(),
279
            'valid' => self::valid()->count(),
280
            'expired' => self::expired()->count(),
281
            'used' => self::used()->count(),
282
            'unused' => self::unused()->count(),
283
        ];
284
    }
285
}
286