124 lines
5.2 KiB
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;
|
|
}
|
|
}
|