where('role', 'responsible')->count(); $explained = $this->explainedPairs(); $unexplained = $this->responsibleRows() ->reject(fn (object $row): bool => isset($explained[$row->company_id.':'.$row->user_id])) ->values(); $this->info(sprintf('%d responsible-Zuordnungen, davon %d über eine hinterlegte E-Mail erklärbar, %d nicht.', $total, count($explained), $unexplained->count())); $since = (string) $this->option('since'); $recent = $unexplained->filter(fn (object $row): bool => (string) $row->created_at >= $since); if ($recent->isNotEmpty()) { $this->line(sprintf('Nicht erklärbar und seit %s entstanden (%d, max. 50 gezeigt):', $since, $recent->count())); $this->table( ['User', 'E-Mail', 'Firma', 'Firmenname', 'Herkunft', 'Angelegt'], $recent->take(50)->map(fn (object $row): array => [ $row->user_id, $row->email, $row->company_id, mb_strimwidth((string) $row->company_name, 0, 40, '…'), $row->granted_via, substr((string) $row->created_at, 0, 16), ])->all(), ); } if ($unexplained->isNotEmpty()) { $file = sprintf('company-memberships-unexplained-%s.csv', now()->format('Y-m-d-His')); $lines = $unexplained->map(fn (object $row): string => implode(';', [ $row->user_id, $row->email, $row->company_id, str_replace(';', ',', (string) $row->company_name), $row->granted_via, $row->created_at, ])); Storage::disk('local')->put($file, "user_id;email;company_id;company_name;granted_via;created_at\n".$lines->implode("\n")); $this->line(sprintf('Vollständige Liste: storage/app/%s', $file)); } if (! $this->option('classify')) { $this->info('Keine Änderung geschrieben. Mit --classify werden die erklärbaren Zeilen als auto markiert.'); return self::SUCCESS; } $before = (string) $this->option('before'); $candidates = $this->responsibleRows() ->filter(fn (object $row): bool => isset($explained[$row->company_id.':'.$row->user_id])) ->where('granted_via', 'manual') ->filter(fn (object $row): bool => $before === '' || (string) $row->created_at < $before) ->values(); $this->line(sprintf( 'Umzustufen auf auto: %d Zuordnungen%s (Stichprobe, max. 20):', $candidates->count(), $before !== '' ? sprintf(' vor %s', $before) : '', )); $this->table( ['User', 'E-Mail', 'Firma', 'Firmenname', 'Herkunft', 'Angelegt'], $candidates->take(20)->map(fn (object $row): array => [ $row->user_id, $row->email, $row->company_id, mb_strimwidth((string) $row->company_name, 0, 40, '…'), $row->granted_via, substr((string) $row->created_at, 0, 16), ])->all(), ); $marked = 0; foreach ($candidates->chunk(500) as $chunk) { foreach ($chunk as $row) { $marked += DB::table('company_user') ->where('company_id', $row->company_id) ->where('user_id', $row->user_id) ->where('role', 'responsible') ->where('granted_via', 'manual') ->update(['granted_via' => 'auto']); } } $this->info(sprintf('%d Zuordnungen als automatisch (auto) markiert.', $marked)); return self::SUCCESS; } /** * Alle responsible-Zeilen mit Konto-Adresse und Firmenname. * * @return Collection */ private function responsibleRows(): Collection { return DB::table('company_user') ->join('users', 'users.id', '=', 'company_user.user_id') ->join('companies', 'companies.id', '=', 'company_user.company_id') ->where('company_user.role', 'responsible') ->orderBy('company_user.created_at') ->get([ 'company_user.company_id', 'company_user.user_id', 'company_user.granted_via', 'company_user.created_at', 'users.email', 'companies.name as company_name', ]); } /** * (company_id, user_id)-Paare, die über Firmen- oder Kontakt-Adresse * erklärbar sind. Einmaliger Lauf, daher LOWER() beidseitig vertretbar. * * @return array */ private function explainedPairs(): array { $viaCompany = DB::table('company_user') ->join('users', 'users.id', '=', 'company_user.user_id') ->join('companies', 'companies.id', '=', 'company_user.company_id') ->where('company_user.role', 'responsible') ->whereRaw('LOWER(companies.email) = LOWER(users.email)'); $viaContact = DB::table('company_user') ->join('users', 'users.id', '=', 'company_user.user_id') ->join('contacts', function ($join): void { $join->on('contacts.company_id', '=', 'company_user.company_id') ->whereRaw('LOWER(contacts.email) = LOWER(users.email)') ->whereNull('contacts.deleted_at'); }) ->where('company_user.role', 'responsible'); $pairs = []; foreach ([$viaCompany, $viaContact] as $query) { foreach ($query->distinct()->get(['company_user.company_id', 'company_user.user_id']) as $row) { $pairs[$row->company_id.':'.$row->user_id] = ['company_id' => (int) $row->company_id, 'user_id' => (int) $row->user_id]; } } return $pairs; } }