NNTmux /
newznab-tmux
| 1 | <?php |
||
| 2 | |||
| 3 | namespace App\Services; |
||
| 4 | |||
| 5 | use App\Mail\InvitationMail; |
||
| 6 | use App\Models\Invitation; |
||
| 7 | use App\Models\User; |
||
| 8 | use Illuminate\Support\Facades\Mail; |
||
| 9 | use Illuminate\Support\Facades\Validator; |
||
| 10 | use Illuminate\Validation\ValidationException; |
||
| 11 | |||
| 12 | class InvitationService |
||
| 13 | { |
||
| 14 | /** |
||
| 15 | * Create and send an invitation |
||
| 16 | */ |
||
| 17 | public function createAndSendInvitation( |
||
| 18 | string $email, |
||
| 19 | int $invitedBy, |
||
| 20 | int $expiryDays = Invitation::DEFAULT_INVITE_EXPIRY_DAYS, |
||
| 21 | array $metadata = [] |
||
| 22 | ): Invitation { |
||
| 23 | // Get the user sending the invitation |
||
| 24 | $user = User::find($invitedBy); |
||
| 25 | if (! $user) { |
||
| 26 | throw new \Exception('User not found.'); |
||
| 27 | } |
||
| 28 | |||
| 29 | // Calculate how many invites are currently "in use" (pending/active) |
||
| 30 | $activeInvitations = Invitation::where('invited_by', $invitedBy) |
||
| 31 | ->where('is_active', true) |
||
| 32 | ->where('used_at', null) |
||
| 33 | ->where('expires_at', '>', now()) |
||
| 34 | ->count(); |
||
| 35 | |||
| 36 | // Check if user has invites available (total invites - active pending invitations) |
||
| 37 | $availableInvites = $user->invites - $activeInvitations; |
||
| 38 | if ($availableInvites <= 0) { |
||
| 39 | throw new \Exception('You have no invitations available. You have '.$activeInvitations.' pending invitation(s). Contact an administrator if you need more invitations.'); |
||
| 40 | } |
||
| 41 | |||
| 42 | // Validate email |
||
| 43 | $validator = Validator::make(['email' => $email], [ |
||
| 44 | 'email' => 'required|email|unique:users,email', |
||
| 45 | ]); |
||
| 46 | |||
| 47 | if ($validator->fails()) { |
||
| 48 | throw new ValidationException($validator); |
||
| 49 | } |
||
| 50 | |||
| 51 | // Check if there's already a valid invitation for this email |
||
| 52 | $existingInvitation = Invitation::where('email', strtolower(trim($email))) |
||
| 53 | ->valid() |
||
| 54 | ->first(); |
||
| 55 | |||
| 56 | if ($existingInvitation) { |
||
| 57 | throw new \Exception('A valid invitation already exists for this email address.'); |
||
| 58 | } |
||
| 59 | |||
| 60 | // Create the invitation (don't decrement invites here - only when used) |
||
| 61 | $invitation = Invitation::createInvitation($email, $invitedBy, $expiryDays, $metadata); |
||
| 62 | |||
| 63 | // Send the invitation email |
||
| 64 | $this->sendInvitationEmail($invitation); |
||
| 65 | |||
| 66 | return $invitation; |
||
| 67 | } |
||
| 68 | |||
| 69 | /** |
||
| 70 | * Send invitation email |
||
| 71 | */ |
||
| 72 | public function sendInvitationEmail(Invitation $invitation): void |
||
| 73 | { |
||
| 74 | Mail::to($invitation->email)->send(new InvitationMail($invitation)); |
||
| 75 | } |
||
| 76 | |||
| 77 | /** |
||
| 78 | * Resend an invitation |
||
| 79 | */ |
||
| 80 | public function resendInvitation(int $invitationId): Invitation |
||
| 81 | { |
||
| 82 | $invitation = Invitation::findOrFail($invitationId); |
||
| 83 | |||
| 84 | if (! $invitation->isValid()) { |
||
| 85 | throw new \Exception('Cannot resend an invalid invitation.'); |
||
| 86 | } |
||
| 87 | |||
| 88 | $this->sendInvitationEmail($invitation); |
||
|
0 ignored issues
–
show
Bug
introduced
by
Loading history...
|
|||
| 89 | |||
| 90 | return $invitation; |
||
|
0 ignored issues
–
show
|
|||
| 91 | } |
||
| 92 | |||
| 93 | /** |
||
| 94 | * Cancel an invitation |
||
| 95 | */ |
||
| 96 | public function cancelInvitation(int $invitationId): bool |
||
| 97 | { |
||
| 98 | $invitation = Invitation::findOrFail($invitationId); |
||
| 99 | |||
| 100 | if ($invitation->isUsed()) { |
||
| 101 | throw new \Exception('Cannot cancel a used invitation.'); |
||
| 102 | } |
||
| 103 | |||
| 104 | $invitation->is_active = false; |
||
|
0 ignored issues
–
show
|
|||
| 105 | |||
| 106 | return $invitation->save(); |
||
| 107 | } |
||
| 108 | |||
| 109 | /** |
||
| 110 | * Accept an invitation (use it for registration) |
||
| 111 | */ |
||
| 112 | public function acceptInvitation(string $token, array $userData): User |
||
| 113 | { |
||
| 114 | $invitation = Invitation::findValidByToken($token); |
||
| 115 | |||
| 116 | if (! $invitation) { |
||
| 117 | throw new \Exception('Invalid or expired invitation token.'); |
||
| 118 | } |
||
| 119 | |||
| 120 | // Get the user who sent the invitation |
||
| 121 | $inviter = User::find($invitation->invited_by); |
||
| 122 | |||
| 123 | // Create the user |
||
| 124 | $user = User::create(array_merge($userData, [ |
||
| 125 | 'email' => $invitation->email, |
||
| 126 | 'invited_by' => $invitation->invited_by, |
||
| 127 | ])); |
||
| 128 | |||
| 129 | // Mark invitation as used |
||
| 130 | $invitation->markAsUsed($user->id); |
||
| 131 | |||
| 132 | // Now decrement the inviter's available invites (only when actually used) |
||
| 133 | if ($inviter) { |
||
| 134 | $inviter->decrement('invites'); |
||
| 135 | } |
||
| 136 | |||
| 137 | return $user; |
||
| 138 | } |
||
| 139 | |||
| 140 | /** |
||
| 141 | * Get invitation statistics for a user |
||
| 142 | */ |
||
| 143 | public function getUserInvitationStats(int $userId): array |
||
| 144 | { |
||
| 145 | return [ |
||
| 146 | 'sent' => Invitation::where('invited_by', $userId)->count(), |
||
| 147 | 'pending' => Invitation::where('invited_by', $userId)->valid()->count(), |
||
| 148 | 'used' => Invitation::where('invited_by', $userId)->used()->count(), |
||
| 149 | 'expired' => Invitation::where('invited_by', $userId)->expired()->count(), |
||
| 150 | ]; |
||
| 151 | } |
||
| 152 | |||
| 153 | /** |
||
| 154 | * Get all invitations for a user |
||
| 155 | */ |
||
| 156 | public function getUserInvitations(int $userId, ?string $status = null) |
||
| 157 | { |
||
| 158 | $query = Invitation::where('invited_by', $userId) |
||
| 159 | ->with(['usedBy']) |
||
| 160 | ->orderBy('created_at', 'desc'); |
||
| 161 | |||
| 162 | return match ($status) { |
||
| 163 | 'valid' => $query->valid()->paginate(15), |
||
| 164 | 'used' => $query->used()->paginate(15), |
||
| 165 | 'expired' => $query->expired()->paginate(15), |
||
| 166 | 'pending' => $query->unused()->paginate(15), |
||
| 167 | default => $query->paginate(15), |
||
| 168 | }; |
||
| 169 | } |
||
| 170 | |||
| 171 | /** |
||
| 172 | * Clean up expired invitations |
||
| 173 | */ |
||
| 174 | public function cleanupExpiredInvitations(): int |
||
| 175 | { |
||
| 176 | return Invitation::cleanupExpired(); |
||
| 177 | } |
||
| 178 | |||
| 179 | /** |
||
| 180 | * Check if a user can send more invitations |
||
| 181 | */ |
||
| 182 | public function canUserSendInvitation(int $userId, ?int $maxInvitations = null): bool |
||
| 183 | { |
||
| 184 | if ($maxInvitations === null) { |
||
| 185 | return true; // No limit set |
||
| 186 | } |
||
| 187 | |||
| 188 | $sentCount = Invitation::where('invited_by', $userId)->count(); |
||
| 189 | |||
| 190 | return $sentCount < $maxInvitations; |
||
| 191 | } |
||
| 192 | |||
| 193 | /** |
||
| 194 | * Get invitation by token for preview |
||
| 195 | */ |
||
| 196 | public function getInvitationPreview(string $token): ?array |
||
| 197 | { |
||
| 198 | $invitation = Invitation::findByToken($token); |
||
| 199 | |||
| 200 | if (! $invitation) { |
||
|
0 ignored issues
–
show
|
|||
| 201 | return null; |
||
| 202 | } |
||
| 203 | |||
| 204 | return [ |
||
| 205 | 'email' => $invitation->email, |
||
| 206 | 'invited_by' => $invitation->invitedBy->username ?? 'Unknown', |
||
| 207 | 'expires_at' => $invitation->expires_at, |
||
| 208 | 'is_valid' => $invitation->isValid(), |
||
| 209 | 'is_expired' => $invitation->isExpired(), |
||
| 210 | 'is_used' => $invitation->isUsed(), |
||
| 211 | 'metadata' => $invitation->metadata, |
||
| 212 | ]; |
||
| 213 | } |
||
| 214 | |||
| 215 | /** |
||
| 216 | * Get detailed invitation information for a user (for debugging) |
||
| 217 | */ |
||
| 218 | public function getUserInvitationDetails(int $userId): array |
||
| 219 | { |
||
| 220 | $user = User::find($userId); |
||
| 221 | if (! $user) { |
||
| 222 | return []; |
||
| 223 | } |
||
| 224 | |||
| 225 | $totalInvites = $user->invites; |
||
| 226 | |||
| 227 | $activeInvitations = Invitation::where('invited_by', $userId) |
||
| 228 | ->where('is_active', true) |
||
| 229 | ->where('used_at', null) |
||
| 230 | ->where('expires_at', '>', now()) |
||
| 231 | ->count(); |
||
| 232 | |||
| 233 | $usedInvitations = Invitation::where('invited_by', $userId) |
||
| 234 | ->whereNotNull('used_at') |
||
| 235 | ->count(); |
||
| 236 | |||
| 237 | $expiredInvitations = Invitation::where('invited_by', $userId) |
||
| 238 | ->where('expires_at', '<=', now()) |
||
| 239 | ->where('used_at', null) |
||
| 240 | ->count(); |
||
| 241 | |||
| 242 | $cancelledInvitations = Invitation::where('invited_by', $userId) |
||
| 243 | ->where('is_active', false) |
||
| 244 | ->where('used_at', null) |
||
| 245 | ->count(); |
||
| 246 | |||
| 247 | $availableInvites = $totalInvites - $activeInvitations; |
||
| 248 | |||
| 249 | return [ |
||
| 250 | 'user_id' => $userId, |
||
| 251 | 'username' => $user->username, |
||
| 252 | 'total_invites' => $totalInvites, |
||
| 253 | 'active_pending' => $activeInvitations, |
||
| 254 | 'used_invitations' => $usedInvitations, |
||
| 255 | 'expired_invitations' => $expiredInvitations, |
||
| 256 | 'cancelled_invitations' => $cancelledInvitations, |
||
| 257 | 'calculated_available' => $availableInvites, |
||
| 258 | 'can_send_invite' => $availableInvites > 0, |
||
| 259 | ]; |
||
| 260 | } |
||
| 261 | } |
||
| 262 |