mivita/app/Services/UserShopService.php
Kevin 20b611c4f4 Standard-Shop fuer jeden Berater: Kundenbestellung ohne Shop crashte nicht mehr
Berater ohne UserShop liefen bei Kundenbestellungen (abo-ot-customer) in
"Attempt to read property id on null". Shops werden jetzt bei Freischaltung
und bei Bedarf automatisch angelegt (Name aus Vor-/Nachname, bei Belegung
-2, -3, ...). Nachtrag fuer Bestandsberater: php artisan user:ensure-shops.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-28 08:13:29 +00:00

102 lines
2.9 KiB
PHP

<?php
namespace App\Services;
use App\Models\UserShop;
use App\User;
use Illuminate\Support\Str;
class UserShopService
{
private const NAME_MIN_LENGTH = 4;
private const NAME_MAX_LENGTH = 20;
private const MAX_COUNTER = 999;
/**
* Jeder Berater hat einen Shop als Verkaufskanal. Fehlt er, wird ein aktiver Standard-Shop
* angelegt; den Namen (Subdomain) kann der Berater später unter "Mein Shop" ändern.
*/
public static function ensureForUser(User $user): UserShop
{
$existing = $user->shop()->first();
if ($existing) {
return $existing;
}
$userShop = UserShop::create([
'user_id' => $user->id,
'name' => self::generateDefaultName($user),
'active' => true,
'active_date' => now(),
]);
$user->setRelation('shop', $userShop);
return $userShop;
}
/**
* Belegte Namen (auch von gelöschten Shops) bekommen eine fortlaufende Endung: -2, -3, ...
*
* @param array<int, string> $reserved Namen, die zusätzlich als belegt gelten (z. B. im Dry-Run)
*/
public static function generateDefaultName(User $user, array $reserved = []): string
{
$account = $user->account;
$base = $account ? Str::slug($account->first_name.' '.$account->last_name, '-', 'de') : '';
$base = self::limitAtWordBoundary($base, self::NAME_MAX_LENGTH);
if (strlen($base) < self::NAME_MIN_LENGTH) {
$base = 'berater';
}
if (self::isNameAvailable($base, $reserved)) {
return $base;
}
for ($counter = 2; $counter <= self::MAX_COUNTER; $counter++) {
$suffix = '-'.$counter;
$candidate = self::limitAtWordBoundary($base, self::NAME_MAX_LENGTH - strlen($suffix)).$suffix;
if (self::isNameAvailable($candidate, $reserved)) {
return $candidate;
}
}
return 'berater-'.$user->id;
}
private static function limitAtWordBoundary(string $slug, int $maxLength): string
{
if (strlen($slug) <= $maxLength) {
return $slug;
}
$boundary = strrpos(substr($slug, 0, $maxLength + 1), '-');
return rtrim($boundary ? substr($slug, 0, $boundary) : substr($slug, 0, $maxLength), '-');
}
/**
* @param array<int, string> $reserved
*/
private static function isNameAvailable(string $name, array $reserved = []): bool
{
if (strlen($name) < self::NAME_MIN_LENGTH || strlen($name) > self::NAME_MAX_LENGTH) {
return false;
}
if (in_array($name, $reserved, true)) {
return false;
}
if (in_array($name, config('profanity.full_word_check', []), true)) {
return false;
}
return ! UserShop::withTrashed()
->where(fn ($query) => $query->where('name', $name)->orWhere('slug', $name))
->exists();
}
}