CluPilotCloud/app/Services/Billing/DunningMailer.php

115 lines
3.8 KiB
PHP

<?php
namespace App\Services\Billing;
use App\Mail\CloudResumedMail;
use App\Mail\CloudSuspendedMail;
use App\Mail\DunningNoticeMail;
use App\Models\DunningCase;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Mail;
use Throwable;
/**
* Wer bei einem Mahnfall was erfährt — an einer Stelle.
*
* Der Versand liegt nicht im Tageslauf und nicht in den Actions, weil er an
* DREI Stellen ausgelöst wird (Fall eröffnet, Stufe erreicht, Fall beglichen)
* und dreimal dieselbe Frage beantworten muss: an welche Adresse, mit welchem
* Betrag, mit welchen Gebühren.
*
* **Ein Versand darf nie den Vorgang kippen.** Ein Mailserver, der gerade nicht
* antwortet, darf keinen Fall daran hindern, weiterzurücken — sonst bleibt der
* Kunde auf der Stufe stehen, auf der die Mail scheiterte, und die Sperre
* kommt nie. Deshalb wird geloggt und geschluckt; die Mail selbst ist ohnehin
* `ShouldQueue` und wird dort erneut versucht.
*/
class DunningMailer
{
public function level(DunningCase $case, int $level): void
{
$customer = $case->subscription?->customer;
$address = $customer?->email;
if (blank($address)) {
return;
}
$gebuehren = $this->feesSoFar($case);
$this->send($address, new DunningNoticeMail(
name: (string) ($customer->name ?? ''),
level: $level,
amountCents: (int) ($case->subscription->price_cents ?? 0),
feeCents: DunningSchedule::feeCents($level),
feeTotalCents: $gebuehren,
currency: 'EUR',
billingUrl: route('billing'),
// Das Datum, an dem abgeschaltet wird — aber nur, solange es noch
// bevorsteht. Eine Drohung ohne Termin ist keine Auskunft, und ein
// Termin in der Vergangenheit wäre schlimmer als keiner.
suspendOn: $level < DunningSchedule::SUSPENDED
? $case->opened_at->copy()
->addDays(DunningSchedule::dayOfLevel(DunningSchedule::SUSPENDED))
->local()->isoFormat('D. MMMM YYYY')
: null,
));
}
public function suspended(DunningCase $case): void
{
$customer = $case->subscription?->customer;
if (blank($customer?->email)) {
return;
}
$this->send($customer->email, new CloudSuspendedMail(
name: (string) ($customer->name ?? ''),
amountCents: (int) ($case->subscription->price_cents ?? 0),
feeTotalCents: $this->feesSoFar($case),
currency: 'EUR',
billingUrl: route('billing'),
));
}
public function resumed(DunningCase $case): void
{
$customer = $case->subscription?->customer;
if (blank($customer?->email)) {
return;
}
$this->send($customer->email, new CloudResumedMail(
name: (string) ($customer->name ?? ''),
// Beglichen ist beglichen: keine Zahl mehr, die nach Restschuld
// aussieht.
amountCents: 0,
feeTotalCents: 0,
currency: 'EUR',
billingUrl: route('cloud'),
));
}
/** Alle bisher angefallenen Mahnspesen dieses Falls. */
private function feesSoFar(DunningCase $case): int
{
return collect(array_keys($case->fee_invoice_ids ?? []))
->sum(fn ($level) => DunningSchedule::feeCents((int) $level));
}
private function send(string $address, object $mailable): void
{
try {
Mail::to($address)->queue($mailable);
} catch (Throwable $e) {
// Siehe Kopfkommentar: laut, aber nicht kippend.
Log::error('Eine Mahnungs-Mail liess sich nicht einreihen', [
'mailable' => $mailable::class,
'exception' => $e->getMessage(),
]);
}
}
}