mivita/app/Console/Commands/UserCleanUp.php
Kevin 0b8f8abeeb Berater faellt aus (B2): pausieren nach 2 Tagen, Rueckgabe bei Zahlung, harter Cut am 28. Tag
Entwicklungsplan September, B2 (Protokoll 08.09., Rueckmeldung 14.09., R3):
- Ablauftag + 2 Kalendertage (Nachtlauf): Team, Kunden, Kundenabos und
  Portal-Logins gehen umkehrbar an den naechsten aktiven Sponsor, jede
  Umhaengung in consultant_suspension_transfers protokolliert. Kurzinfo
  an den Sponsor ohne Listen, keine Mail an das Firmenkonto.
- Zahlung bis Tag 27: Berater aktiv, Team und Kundendaten zurueck (nur
  was noch beim Sponsor liegt); Umsaetze der Zwischenzeit bleiben dort.
- Tag 28: was noch am Berater haengt geht endgueltig an den Sponsor,
  Berater wird dessen Kunde (ohne Duplikat) und geloescht; Listen-Mail
  mit Vorname, Nachname, Telefon, E-Mail an jeden haltenden Sponsor.
- Cron, Zahlungseingang und Admin-Ablaufdatum laufen ueber
  ConsultantSuspensionService; user:cleanup --dry-run listet beide Stufen.
- Mail 35 und Dashboard nennen Pausentag und Frist; Fristen in
  config/mivita.php (cleanup_suspend_days, cleanup_delete_days).
- Nebenbefunde: Kernel referenziert UserCleanUp, user:restore aus der
  User-Bereinigung laeuft nur noch einmal.
- Testschema: shopping_users.auth_user_id nullable wie auf Produktion
  (Migration aendert nur, wo die Spalte noch NOT NULL ist).
- Uebersicht fuer die Kundin: dev/_tickets/2026-09-23-mitgliedschaft-
  ablauf-uebersicht.md.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-23 15:44:45 +00:00

101 lines
3.5 KiB
PHP

<?php
namespace App\Console\Commands;
use App\Services\ConsultantSuspensionService;
use App\User;
use Carbon\Carbon;
use Illuminate\Console\Command;
/**
* Nächtlicher Ablauf für Berater mit abgelaufener Mitgliedschaft (Entwicklungsplan B2).
*
* Reihenfolge wie bisher: erst der harte Cut (Ablauf + `mivita.cleanup_delete_days`),
* dann das Pausieren (Ablauf + `mivita.cleanup_suspend_days`). Die Arbeit macht
* {@see ConsultantSuspensionService}; dieser Command wählt nur die Kandidaten aus.
*/
class UserCleanUp extends Command
{
/**
* @var string
*/
protected $signature = 'user:cleanup
{--dry-run : Nur auflisten, was passieren würde, nichts ändern und keine Mails senden}';
/**
* @var string
*/
protected $description = 'Berater mit abgelaufener Mitgliedschaft pausieren (Team und Kunden an den Sponsor) bzw. nach der Frist endgültig entfernen';
public function handle(ConsultantSuspensionService $suspension): int
{
$dryRun = (bool) $this->option('dry-run');
$this->info('RUN Command user:cleanup'.($dryRun ? ' (dry-run)' : ''));
\Log::channel('cleanup')->info('COMMAND [user:cleanup] started.'.($dryRun ? ' dry-run' : ''));
$this->runStage(
'Harter Cut (Ablauf vor mind. '.$suspension->deleteDays().' Tagen)',
$suspension->terminationCandidates(),
$suspension,
$dryRun,
fn (User $user) => $suspension->terminate($user),
);
$this->runStage(
'Pausieren (Ablauf vor mind. '.$suspension->suspendDays().' Tagen)',
$suspension->suspensionCandidates(),
$suspension,
$dryRun,
fn (User $user) => $suspension->suspend($user, 'user:cleanup'),
);
\Log::channel('cleanup')->info('COMMAND [user:cleanup] finished.');
return self::SUCCESS;
}
/**
* @param iterable<User> $candidates
* @param callable(User): (array<string, int>|null) $action
*/
private function runStage(string $label, iterable $candidates, ConsultantSuspensionService $suspension, bool $dryRun, callable $action): void
{
$rows = [];
$done = 0;
$failed = 0;
foreach ($candidates as $user) {
$counts = $dryRun ? $suspension->preview($user) : null;
if (! $dryRun) {
try {
$counts = $action($user);
} catch (\Throwable $e) {
\Log::channel('cleanup')->error('user:cleanup '.$label.' fehlgeschlagen fuer user_id '.$user->id.': '.$e->getMessage());
$counts = null;
}
$counts === null ? $failed++ : $done++;
}
$rows[] = [
$user->id,
trim($user->account?->first_name.' '.$user->account?->last_name),
$user->email,
$user->payment_account ? Carbon::parse($user->payment_account)->format('d.m.Y') : '',
$counts['team'] ?? '-',
$counts['customers'] ?? '-',
$counts['abos'] ?? '-',
$counts['portal_customers'] ?? '-',
$counts['parked'] ?? '-',
];
}
$this->line('');
$this->info($label.': '.count($rows).' Berater'.($dryRun ? '' : " (erledigt {$done}, übersprungen {$failed})"));
if ($rows !== []) {
$this->table(['ID', 'Name', 'E-Mail', 'Ablauf', 'Team', 'Kunden', 'Kundenabos', 'Portal-Logins', 'beim Sponsor geparkt'], $rows);
}
}
}