Abos: taegliche Warnung fuer ueberfaellige Abos, ohne Nachbuchung

user:make_abo_order nimmt nur Abos mit next_date = heute; faellt ein Lauf
aus, bleibt ein Abo stillschweigend liegen (Abo 39: Termin 25.03.2026).
abo:check-overdue meldet taeglich um 05:30 alle aktiven Abos mit Status
okay und verstrichenem Termin per Mail an mivita (Link ins Admin-Abo) und
ins abo_order-Log. Es wird bewusst nichts nachgebucht; --no-mail nur fuer
die Ausgabe.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Kevin 2026-09-23 15:44:45 +00:00
parent 0b8f8abeeb
commit 452d1ac17f
5 changed files with 288 additions and 0 deletions

View file

@ -0,0 +1,104 @@
<?php
namespace App\Console\Commands;
use App\Mail\AboOverdueAlertMail;
use App\Models\UserAbo;
use Carbon\Carbon;
use Illuminate\Console\Command;
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Mail;
/**
* Warnt vor laufenden Abos, deren nächste Ausführung in der Vergangenheit liegt.
*
* `user:make_abo_order` nimmt nur Abos mit `next_date = heute`. Fällt ein Lauf aus
* oder bricht ein Abo ab, ohne dass sein Termin weitergesetzt wird, bleibt es
* stillschweigend liegen (Befund Abo 39: Termin 25.03.2026, bis September nicht
* ausgeführt). Dieser Command bucht bewusst nichts nach — er meldet nur, damit
* jemand entscheidet, ob nachgeliefert, der Termin korrigiert oder das Abo beendet wird.
*/
class AboCheckOverdue extends Command
{
/**
* @var string
*/
protected $signature = 'abo:check-overdue
{--no-mail : Nur ausgeben und loggen, keine Mail}';
/**
* @var string
*/
protected $description = 'Laufende Abos mit einem Ausführungstermin in der Vergangenheit melden (ohne Nachbuchung)';
public function handle(): int
{
$overdue = $this->overdueAbos();
if ($overdue->isEmpty()) {
$this->info('Keine überfälligen Abos.');
Log::channel('abo_order')->info('abo:check-overdue: keine ueberfaelligen Abos');
return self::SUCCESS;
}
$this->warn('Überfällige Abos: '.$overdue->count());
$this->table(
['Abo', 'Art', 'Kunde/Berater', 'Termin', 'Tage überfällig', 'Letzte Ausführung'],
$overdue->map(fn (UserAbo $abo) => [
$abo->id,
$abo->is_for,
$this->holderName($abo),
Carbon::parse($abo->next_date)->format('d.m.Y'),
$this->daysOverdue($abo),
$abo->last_date ? Carbon::parse($abo->last_date)->format('d.m.Y') : '–',
])->all(),
);
Log::channel('abo_order')->warning('abo:check-overdue: '.$overdue->count().' ueberfaellige Abos', [
'abo_ids' => $overdue->pluck('id')->all(),
]);
if (! $this->option('no-mail')) {
try {
Mail::to(config('app.default_mail'))->send(new AboOverdueAlertMail($overdue->all()));
} catch (\Throwable $e) {
Log::channel('abo_order')->error('abo:check-overdue: Mail fehlgeschlagen: '.$e->getMessage());
}
}
return self::SUCCESS;
}
/**
* Aktiv, Status „okay“, Termin vor heute — genau die Abos, die der Ausführungslauf
* nie mehr anfassen würde.
*
* @return Collection<int, UserAbo>
*/
private function overdueAbos(): Collection
{
return UserAbo::query()
->with(['shopping_user', 'user.account', 'member.account'])
->where('active', true)
->where('status', 2)
->whereDate('next_date', '<', now()->toDateString())
->orderBy('next_date')
->get();
}
private function daysOverdue(UserAbo $abo): int
{
return (int) Carbon::parse($abo->next_date)->startOfDay()->diffInDays(now()->startOfDay());
}
private function holderName(UserAbo $abo): string
{
if ($abo->is_for === 'me') {
return trim($abo->user?->account?->first_name.' '.$abo->user?->account?->last_name) ?: (string) $abo->email;
}
return trim($abo->shopping_user?->billing_firstname.' '.$abo->shopping_user?->billing_lastname) ?: (string) $abo->email;
}
}