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>
102 lines
2.9 KiB
PHP
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();
|
|
}
|
|
}
|