mirror of
https://github.com/NNTmux/newznab-tmux.git
synced 2026-08-29 09:18:54 +00:00
274 lines
8.0 KiB
PHP
274 lines
8.0 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace App\Services;
|
|
|
|
use App\Mail\InvitationMail;
|
|
use App\Models\Invitation;
|
|
use App\Models\User;
|
|
use Illuminate\Support\Facades\Mail;
|
|
use Illuminate\Support\Facades\Validator;
|
|
use Illuminate\Validation\ValidationException;
|
|
|
|
class InvitationService
|
|
{
|
|
/**
|
|
* Create and send an invitation
|
|
*
|
|
* @param array<string, mixed> $metadata
|
|
*/
|
|
public function createAndSendInvitation(
|
|
string $email,
|
|
int $invitedBy,
|
|
int $expiryDays = Invitation::DEFAULT_INVITE_EXPIRY_DAYS,
|
|
array $metadata = []
|
|
): Invitation {
|
|
// Get the user sending the invitation
|
|
$user = User::find($invitedBy);
|
|
if (! $user) {
|
|
throw new \Exception('User not found.');
|
|
}
|
|
|
|
// Calculate how many invites are currently "in use" (pending/active)
|
|
$activeInvitations = Invitation::where('invited_by', $invitedBy)
|
|
->where('is_active', true)
|
|
->where('used_at', null)
|
|
->where('expires_at', '>', now())
|
|
->count();
|
|
|
|
// Check if user has invites available (total invites - active pending invitations)
|
|
$availableInvites = $user->invites - $activeInvitations;
|
|
if ($availableInvites <= 0) {
|
|
throw new \Exception('You have no invitations available. You have '.$activeInvitations.' pending invitation(s). Contact an administrator if you need more invitations.');
|
|
}
|
|
|
|
// Validate email
|
|
$validator = Validator::make(['email' => $email], [
|
|
'email' => 'required|email|unique:users,email',
|
|
]);
|
|
|
|
if ($validator->fails()) {
|
|
throw new ValidationException($validator);
|
|
}
|
|
|
|
// Check if there's already a valid invitation for this email
|
|
$existingInvitation = Invitation::where('email', strtolower(trim($email)))
|
|
->valid()
|
|
->first();
|
|
|
|
if ($existingInvitation) {
|
|
throw new \Exception('A valid invitation already exists for this email address.');
|
|
}
|
|
|
|
// Create the invitation (don't decrement invites here - only when used)
|
|
$invitation = Invitation::createInvitation($email, $invitedBy, $expiryDays, $metadata);
|
|
|
|
// Send the invitation email
|
|
$this->sendInvitationEmail($invitation);
|
|
|
|
return $invitation;
|
|
}
|
|
|
|
/**
|
|
* Send invitation email
|
|
*/
|
|
public function sendInvitationEmail(Invitation $invitation): void
|
|
{
|
|
Mail::to($invitation->email)->send(new InvitationMail($invitation));
|
|
}
|
|
|
|
/**
|
|
* Resend an invitation
|
|
*/
|
|
public function resendInvitation(int $invitationId): Invitation
|
|
{
|
|
$invitation = Invitation::findOrFail($invitationId);
|
|
|
|
if (! $invitation->isValid()) {
|
|
throw new \Exception('Cannot resend an invalid invitation.');
|
|
}
|
|
|
|
$this->sendInvitationEmail($invitation);
|
|
|
|
return $invitation;
|
|
}
|
|
|
|
/**
|
|
* Cancel an invitation
|
|
*/
|
|
public function cancelInvitation(int $invitationId): bool
|
|
{
|
|
$invitation = Invitation::findOrFail($invitationId);
|
|
|
|
if ($invitation->isUsed()) {
|
|
throw new \Exception('Cannot cancel a used invitation.');
|
|
}
|
|
|
|
$invitation->is_active = false;
|
|
|
|
return $invitation->save();
|
|
}
|
|
|
|
/**
|
|
* Accept an invitation (use it for registration)
|
|
*
|
|
* @param array<string, mixed> $userData
|
|
*/
|
|
public function acceptInvitation(string $token, array $userData): User
|
|
{
|
|
$invitation = Invitation::findValidByToken($token);
|
|
|
|
if (! $invitation) {
|
|
throw new \Exception('Invalid or expired invitation token.');
|
|
}
|
|
|
|
// Get the user who sent the invitation
|
|
$inviter = User::find($invitation->invited_by);
|
|
|
|
// Create the user
|
|
$user = User::create(array_merge($userData, [
|
|
'email' => $invitation->email,
|
|
'invited_by' => $invitation->invited_by,
|
|
]));
|
|
|
|
// Mark invitation as used
|
|
$invitation->markAsUsed($user->id);
|
|
|
|
// Now decrement the inviter's available invites (only when actually used)
|
|
if ($inviter) {
|
|
$inviter->decrement('invites');
|
|
}
|
|
|
|
return $user;
|
|
}
|
|
|
|
/**
|
|
* Get invitation statistics for a user
|
|
*
|
|
* @return array<string, mixed>
|
|
*/
|
|
public function getUserInvitationStats(int $userId): array
|
|
{
|
|
return [
|
|
'sent' => Invitation::where('invited_by', $userId)->count(),
|
|
'pending' => Invitation::where('invited_by', $userId)->valid()->count(),
|
|
'used' => Invitation::where('invited_by', $userId)->used()->count(),
|
|
'expired' => Invitation::where('invited_by', $userId)->expired()->count(),
|
|
];
|
|
}
|
|
|
|
/**
|
|
* Get all invitations for a user
|
|
*/
|
|
public function getUserInvitations(int $userId, ?string $status = null): mixed
|
|
{
|
|
$query = Invitation::where('invited_by', $userId)
|
|
->with(['usedBy'])
|
|
->orderBy('created_at', 'desc');
|
|
|
|
return match ($status) {
|
|
'valid' => $query->valid()->paginate(15),
|
|
'used' => $query->used()->paginate(15),
|
|
'expired' => $query->expired()->paginate(15),
|
|
'pending' => $query->unused()->paginate(15),
|
|
default => $query->paginate(15),
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Clean up expired invitations
|
|
*/
|
|
public function cleanupExpiredInvitations(): int
|
|
{
|
|
return Invitation::cleanupExpired();
|
|
}
|
|
|
|
/**
|
|
* Check if a user can send more invitations
|
|
*/
|
|
public function canUserSendInvitation(int $userId, ?int $maxInvitations = null): bool
|
|
{
|
|
if ($maxInvitations === null) {
|
|
return true; // No limit set
|
|
}
|
|
|
|
$sentCount = Invitation::where('invited_by', $userId)->count();
|
|
|
|
return $sentCount < $maxInvitations;
|
|
}
|
|
|
|
/**
|
|
* Get invitation by token for preview
|
|
*
|
|
* @return array<string, mixed>
|
|
*/
|
|
public function getInvitationPreview(string $token): ?array
|
|
{
|
|
$invitation = Invitation::findByToken($token);
|
|
|
|
if (! $invitation) {
|
|
return null;
|
|
}
|
|
|
|
return [
|
|
'email' => $invitation->email,
|
|
'invited_by' => $invitation->invitedBy->username ?? 'Unknown',
|
|
'expires_at' => $invitation->expires_at,
|
|
'is_valid' => $invitation->isValid(),
|
|
'is_expired' => $invitation->isExpired(),
|
|
'is_used' => $invitation->isUsed(),
|
|
'metadata' => $invitation->metadata,
|
|
];
|
|
}
|
|
|
|
/**
|
|
* Get detailed invitation information for a user (for debugging)
|
|
*
|
|
* @return array<string, mixed>
|
|
*/
|
|
public function getUserInvitationDetails(int $userId): array
|
|
{
|
|
$user = User::find($userId);
|
|
if (! $user) {
|
|
return [];
|
|
}
|
|
|
|
$totalInvites = $user->invites;
|
|
|
|
$activeInvitations = Invitation::where('invited_by', $userId)
|
|
->where('is_active', true)
|
|
->where('used_at', null)
|
|
->where('expires_at', '>', now())
|
|
->count();
|
|
|
|
$usedInvitations = Invitation::where('invited_by', $userId)
|
|
->whereNotNull('used_at')
|
|
->count();
|
|
|
|
$expiredInvitations = Invitation::where('invited_by', $userId)
|
|
->where('expires_at', '<=', now())
|
|
->where('used_at', null)
|
|
->count();
|
|
|
|
$cancelledInvitations = Invitation::where('invited_by', $userId)
|
|
->where('is_active', false)
|
|
->where('used_at', null)
|
|
->count();
|
|
|
|
$availableInvites = $totalInvites - $activeInvitations;
|
|
|
|
return [
|
|
'user_id' => $userId,
|
|
'username' => $user->username,
|
|
'total_invites' => $totalInvites,
|
|
'active_pending' => $activeInvitations,
|
|
'used_invitations' => $usedInvitations,
|
|
'expired_invitations' => $expiredInvitations,
|
|
'cancelled_invitations' => $cancelledInvitations,
|
|
'calculated_available' => $availableInvites,
|
|
'can_send_invite' => $availableInvites > 0,
|
|
];
|
|
}
|
|
}
|