115 lines
3.8 KiB
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(),
|
|
]);
|
|
}
|
|
}
|
|
}
|