*/ 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 */ 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)); } }