Files
moonwell-web/app/Services/InviteCodeService.php
2026-03-13 23:33:35 +04:00

113 lines
3.0 KiB
PHP

<?php
namespace App\Services;
use App\Exceptions\InviteCodeException;
use App\Models\GameAccountUser;
use App\Models\InviteCode;
use Closure;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Str;
class InviteCodeService
{
/**
* @return \Illuminate\Support\Collection<int, \App\Models\InviteCode>
*/
public function createCodes(int $quantity, GameAccountUser $creator): Collection
{
$created = collect();
for ($index = 0; $index < $quantity; $index++) {
$created->push(InviteCode::query()->create([
'code' => $this->generateUniqueCode(),
'created_by_account_id' => $creator->id,
'created_by_username' => $creator->username,
]));
}
return $created;
}
/**
* @template TReturn
*
* @param Closure(): TReturn $callback
* @return TReturn
*/
public function redeemForRegistration(string $code, string $username, Closure $callback): mixed
{
$normalizedCode = $this->normalizeCode($code);
$normalizedUsername = strtoupper(trim($username));
return DB::transaction(function () use ($callback, $normalizedCode, $normalizedUsername) {
/** @var InviteCode|null $invite */
$invite = InviteCode::query()
->where('code', $normalizedCode)
->lockForUpdate()
->first();
if (! $invite) {
throw InviteCodeException::invalid();
}
if ($invite->redeemed_at !== null) {
throw InviteCodeException::alreadyUsed();
}
$result = $callback();
$invite->forceFill([
'redeemed_by_account_id' => $result['id'],
'redeemed_by_username' => $normalizedUsername,
'redeemed_at' => now(),
])->save();
return $result;
});
}
/**
* @return \Illuminate\Support\Collection<int, \App\Models\InviteCode>
*/
public function latest(int $limit = 100): Collection
{
return InviteCode::query()
->latest('id')
->limit($limit)
->get();
}
/**
* @return array{total: int, available: int, redeemed: int}
*/
public function stats(): array
{
$total = InviteCode::query()->count();
$redeemed = InviteCode::query()->whereNotNull('redeemed_at')->count();
return [
'total' => $total,
'available' => $total - $redeemed,
'redeemed' => $redeemed,
];
}
private function generateUniqueCode(): string
{
do {
$code = $this->normalizeCode(
Str::upper(Str::random(4)).'-'.Str::upper(Str::random(4)).'-'.Str::upper(Str::random(4)),
);
} while (InviteCode::query()->where('code', $code)->exists());
return $code;
}
private function normalizeCode(string $code): string
{
return Str::upper(trim($code));
}
}