CluPilotCloud/app/Console/Commands/RemindEndingServices.php

124 lines
5.2 KiB
PHP

<?php
namespace App\Console\Commands;
use App\Mail\ServiceEndingSoonMail;
use App\Models\Instance;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Mail;
use Throwable;
/**
* Die Erinnerung, bevor EndInstanceService die Adresse einzieht.
*
* Am Laufzeitende zieht EndInstanceService die Route und den DNS-Eintrag
* einer gekündigten Instanz ein — richtig so, das bleibt unangetastet. Die
* Folge, die bisher niemand aussprach: ab genau diesem Moment kommt der Kunde
* selbst nicht mehr an seine Nextcloud. Wer eigene Dateien herunterladen
* will, muss es VORHER tun, und dieser Befehl ist der einzige Ort im Bestand,
* der ihm das vorher sagt.
*
* Das Gegenstück zu App\Console\Commands\EndDueServices — der beendet, was
* fällig ist, dieser hier kündigt an, was demnächst fällig wird. Dieselbe
* Bauform: `--dry-run`, eine Zeile pro betroffener Instanz, eine
* zusammenfassende Meldung am Ende.
*
* ## Die Auswahl: höchstens sieben Tage, nicht genau sieben
*
* Ein Gleichheitsvergleich auf einen Tag verfehlt jede Instanz, die zwischen
* zwei täglichen Läufen durchrutscht — ein Lauf, der einen Tag ausfällt,
* oder eine Instanz, die mit kurzer Frist gekündigt wurde und deren Ende von
* Anfang an weniger als sieben Tage entfernt lag. Der Preis eines verfehlten
* Vergleichs ist, dass jemand ausgesperrt wird, ohne es gewusst zu haben —
* lieber spät erinnert als gar nicht. `export_reminded_at` sorgt dafür, dass
* daraus trotzdem nur EINE Mail wird: eine Erinnerung im Sechserpack liest
* sich als Mahnung.
*/
class RemindEndingServices extends Command
{
protected $signature = 'clupilot:remind-ending-services
{--dry-run : listet auf, wer erinnert würde, und ändert nichts}';
protected $description = 'Erinnert Kunden einmalig ans Selbst-Herunterladen, bevor ihre gekündigte Cloud unerreichbar wird';
public function handle(): int
{
$dryRun = (bool) $this->option('dry-run');
$reminded = 0;
$due = Instance::query()
->with('customer')
->where('status', 'cancellation_scheduled')
->whereNotNull('service_ends_at')
// In der Zukunft und höchstens sieben Tage entfernt — siehe
// Kopfkommentar. Eine bereits abgelaufene Instanz gehört
// EndDueServices, nicht mehr dieser Erinnerung.
->where('service_ends_at', '>', now())
->where('service_ends_at', '<=', now()->addDays(7))
->whereNull('export_reminded_at')
->cursor();
foreach ($due as $instance) {
$customer = $instance->customer;
if ($customer === null || blank($customer->email)) {
// $customer === null ist über die Anwendung praktisch
// unerreichbar: instances.customer_id ist ein
// Pflicht-Fremdschlüssel mit Kaskade, und customers.email ist
// NOT NULL. Trotzdem abgefangen statt vorausgesetzt — eine
// Instanz ohne ansprechbaren Kunden soll den ganzen Lauf
// nicht mitreißen, welcher Zustand sie auch dorthin gebracht
// hätte. Laut, aber ohne export_reminded_at zu setzen: eine
// Mail, die nie eine Adresse hatte, ist keine, die verschickt
// wurde.
Log::warning('Instanz nähert sich dem Laufzeitende, aber kein erreichbarer Kunde dahinter.', [
'instance' => $instance->uuid,
]);
continue;
}
$this->line(($dryRun ? '[dry-run] ' : '')
."{$instance->subdomain}: Laufzeit endet am "
.$instance->service_ends_at->local()->isoFormat('LL'));
if ($dryRun) {
$reminded++;
continue;
}
try {
$mail = new ServiceEndingSoonMail($instance);
Mail::mailer($mail->mailer)->to($customer->email)->queue($mail);
// In DERSELBEN Absicherung wie der Versand, nicht danach:
// hinge ausgerechnet dieser Schreibzugriff (Datenbank kurz
// weg), bliebe die Marke leer, obwohl die Mail schon
// eingereiht ist — derselbe Kunde bekäme morgen eine zweite.
// Und ein ungefangener Fehler hier würde den ganzen Lauf
// abbrechen, statt nur diese eine Instanz zu überspringen —
// die restlichen Instanzen des Tages kämen erst morgen an
// die Reihe, ein Siebtel der sieben Tage Vorlauf verloren.
$instance->update(['export_reminded_at' => now()]);
} catch (Throwable $e) {
Log::warning('Die Erinnerung vor dem Laufzeitende ließ sich nicht einreihen oder nicht vermerken.', [
'instance' => $instance->uuid,
'exception' => $e->getMessage(),
]);
continue;
}
$reminded++;
}
$this->info($dryRun
? "Probelauf: {$reminded} Erinnerung(en) würden verschickt. Nichts wurde geändert."
: "{$reminded} Erinnerung(en) verschickt.");
return self::SUCCESS;
}
}