Anfrage zuerst in den Posteingang, dann erst die Mail: ein gescheiterter Versand darf sie nicht verschlucken
parent
e6ee0716f0
commit
9f6ddc317f
|
|
@ -3,9 +3,12 @@
|
|||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Mail\ContactRequestMail;
|
||||
use App\Models\Customer;
|
||||
use App\Models\InboundMail;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Mail;
|
||||
use Illuminate\Support\Str;
|
||||
use Illuminate\View\View;
|
||||
|
||||
/**
|
||||
|
|
@ -58,14 +61,34 @@ class ContactController extends Controller
|
|||
'message' => 'Nachricht',
|
||||
]);
|
||||
|
||||
Mail::send(new ContactRequestMail([
|
||||
$enquiry = [
|
||||
'company' => $data['company'] ?? null,
|
||||
'name' => $data['name'],
|
||||
'email' => $data['email'],
|
||||
'phone' => $data['phone'] ?? null,
|
||||
'message' => $data['message'],
|
||||
'topic' => $data['topic'] ?? 'general',
|
||||
]));
|
||||
];
|
||||
|
||||
// ZUERST festhalten, dann erst benachrichtigen.
|
||||
//
|
||||
// Die Mail war anfangs die einzige Ablage, und das ging in dem Moment
|
||||
// schief, in dem der Versand scheiterte: der Besucher las „Ihre Anfrage
|
||||
// ist da", und die Anfrage lag in `failed_jobs`, wo niemand nachsieht.
|
||||
// Genau so ist es hier beim ersten Versuch passiert — das Postfach
|
||||
// support@ hatte kein eigenes SMTP-Konto, und der Server wies den
|
||||
// Absender ab.
|
||||
//
|
||||
// Der Posteingang der Konsole ist ohnehin die richtige Ablage: er
|
||||
// existiert für „was Kunden uns geschrieben haben", zeigt Nicht-
|
||||
// Zugeordnetes zuerst und nennt in seinem eigenen Kopfkommentar die
|
||||
// neue Anfrage von einer unbekannten Adresse als den Fall, der nicht
|
||||
// untergehen darf. Eine Website-Anfrage IST dieser Fall.
|
||||
$this->record($enquiry);
|
||||
|
||||
// Die Mail ist ab hier eine Benachrichtigung, kein Ablageort. Scheitert
|
||||
// sie, steht die Anfrage trotzdem in der Konsole.
|
||||
Mail::send(new ContactRequestMail($enquiry));
|
||||
|
||||
// Zurück auf dieselbe Seite, mit einer Fahne in der Sitzung statt einer
|
||||
// Kennung in der Adresse: ein Neuladen zeigt danach wieder das leere
|
||||
|
|
@ -75,4 +98,47 @@ class ContactController extends Controller
|
|||
->route('contact')
|
||||
->with('contact.sent', true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Die Anfrage in den Posteingang der Konsole legen.
|
||||
*
|
||||
* @param array{company: ?string, name: string, email: string, phone: ?string, message: string, topic: ?string} $enquiry
|
||||
*/
|
||||
private function record(array $enquiry): void
|
||||
{
|
||||
InboundMail::create([
|
||||
// Dieselbe Zuordnungsregel wie beim Abholen echter Mail
|
||||
// (IngestInboundMail): schreibt ein bestehender Kunde über das
|
||||
// Formular, hängt die Anfrage an ihm. Sonst bleibt sie ohne
|
||||
// Zuordnung — und steht damit im Posteingang ganz oben.
|
||||
'customer_id' => Customer::query()->where('email', $enquiry['email'])->value('id'),
|
||||
// Synthetisch, weil eine Formularanfrage keine echte Message-ID hat
|
||||
// und die Spalte eindeutig sein muss. Das Präfix macht später
|
||||
// erkennbar, dass dieser Eintrag nicht aus dem Postfach kam.
|
||||
'message_id' => 'web-'.Str::uuid().'@'.(parse_url((string) config('app.url'), PHP_URL_HOST) ?: 'clupilot'),
|
||||
'from_email' => $enquiry['email'],
|
||||
'from_name' => $enquiry['company']
|
||||
? $enquiry['name'].' · '.$enquiry['company']
|
||||
: $enquiry['name'],
|
||||
'subject' => ContactRequestMail::subjectFor($enquiry),
|
||||
'body' => $this->bodyFor($enquiry),
|
||||
'received_at' => now(),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Was der Posteingang als Nachricht zeigt.
|
||||
*
|
||||
* Die Telefonnummer gehört mit hinein: `inbound_mails` hat kein Feld dafür,
|
||||
* und eine Nummer, die jemand extra angegeben hat, im Formular zu verlieren
|
||||
* hieße, ihn nicht anrufen zu können.
|
||||
*
|
||||
* @param array{phone: ?string, message: string} $enquiry
|
||||
*/
|
||||
private function bodyFor(array $enquiry): string
|
||||
{
|
||||
return $enquiry['phone']
|
||||
? $enquiry['message']."\n\n—\nTelefon: ".$enquiry['phone']
|
||||
: $enquiry['message'];
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -65,15 +65,26 @@ class ContactRequestMail extends Mailable implements ShouldQueue
|
|||
]);
|
||||
}
|
||||
|
||||
private function subjectLine(): string
|
||||
{
|
||||
return self::subjectFor($this->enquiry);
|
||||
}
|
||||
|
||||
/**
|
||||
* Der Betreff trägt, wonach gefragt wird, und von wem — beides in der
|
||||
* Postfachliste sichtbar, ohne die Mail zu öffnen.
|
||||
*
|
||||
* Öffentlich und statisch, weil ihn zwei brauchen: diese Mail und der
|
||||
* Eintrag im Posteingang der Konsole. Zwei Fassungen liefen auseinander,
|
||||
* und dann hieße dieselbe Anfrage an zwei Stellen verschieden.
|
||||
*
|
||||
* @param array{company: ?string, name: string, topic: ?string} $enquiry
|
||||
*/
|
||||
private function subjectLine(): string
|
||||
public static function subjectFor(array $enquiry): string
|
||||
{
|
||||
$who = $this->enquiry['company'] ?: $this->enquiry['name'];
|
||||
$who = $enquiry['company'] ?: $enquiry['name'];
|
||||
|
||||
return $this->enquiry['topic'] === 'enterprise'
|
||||
return ($enquiry['topic'] ?? null) === 'enterprise'
|
||||
? 'Anfrage: eigene Maschine — '.$who
|
||||
: 'Anfrage über die Website — '.$who;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,8 @@
|
|||
<?php
|
||||
|
||||
use App\Mail\ContactRequestMail;
|
||||
use App\Models\Customer;
|
||||
use App\Models\InboundMail;
|
||||
use Illuminate\Support\Facades\Mail;
|
||||
|
||||
/**
|
||||
|
|
@ -135,6 +137,73 @@ it('bremst den Versand, statt ihn offen stehen zu lassen', function () {
|
|||
Mail::assertQueuedCount(5);
|
||||
});
|
||||
|
||||
it('legt die Anfrage in den Posteingang der Konsole, nicht nur in eine Mail', function () {
|
||||
$this->post(route('contact.send'), [
|
||||
'company' => 'Kanzlei Muster',
|
||||
'name' => 'Anna Muster',
|
||||
'email' => 'anna@kanzlei-muster.at',
|
||||
'phone' => '+43 1 2345678',
|
||||
'message' => 'Wir haben 40 Mitarbeiter und brauchen etwa 400 GB.',
|
||||
'topic' => 'enterprise',
|
||||
]);
|
||||
|
||||
$eintrag = InboundMail::query()->latest('id')->first();
|
||||
|
||||
expect($eintrag)->not->toBeNull()
|
||||
->and($eintrag->from_email)->toBe('anna@kanzlei-muster.at')
|
||||
->and($eintrag->from_name)->toBe('Anna Muster · Kanzlei Muster')
|
||||
->and($eintrag->subject)->toBe('Anfrage: eigene Maschine — Kanzlei Muster')
|
||||
->and($eintrag->body)->toContain('40 Mitarbeiter')
|
||||
// `inbound_mails` hat kein Feld für die Nummer. Sie im Formular zu
|
||||
// verlieren hieße, den Fragenden nicht anrufen zu können.
|
||||
->and($eintrag->body)->toContain('+43 1 2345678')
|
||||
// Eine fremde Adresse bleibt ohne Zuordnung — und steht damit im
|
||||
// Posteingang ganz oben, wo eine neue Anfrage hingehört.
|
||||
->and($eintrag->customer_id)->toBeNull();
|
||||
});
|
||||
|
||||
it('hängt die Anfrage an den Kunden, wenn die Adresse schon bekannt ist', function () {
|
||||
$kunde = Customer::factory()->create(['email' => 'bestand@example.at']);
|
||||
|
||||
$this->post(route('contact.send'), [
|
||||
'name' => 'Bestandskunde',
|
||||
'email' => 'bestand@example.at',
|
||||
'message' => 'Ich brauche mehr Speicher als mein Paket hergibt.',
|
||||
]);
|
||||
|
||||
expect(InboundMail::query()->latest('id')->value('customer_id'))->toBe($kunde->id);
|
||||
});
|
||||
|
||||
it('behält die Anfrage, auch wenn der Versand scheitert', function () {
|
||||
// Der Fall, der das alles ausgelöst hat: support@ hatte kein eigenes
|
||||
// SMTP-Konto, der Server wies den Absender ab, die Anfrage lag in
|
||||
// `failed_jobs` — und der Besucher hatte „Ihre Anfrage ist da" gelesen.
|
||||
Mail::shouldReceive('send')->once()->andThrow(new RuntimeException('553 Sender address rejected'));
|
||||
|
||||
// Der Versand fliegt dem Besucher um die Ohren, statt ihm eine Bestätigung
|
||||
// für etwas zu zeigen, das nicht angekommen ist.
|
||||
$this->post(route('contact.send'), [
|
||||
'name' => 'Anna Muster',
|
||||
'email' => 'anna@example.at',
|
||||
'message' => 'Bitte melden Sie sich bei mir.',
|
||||
])->assertStatus(500);
|
||||
|
||||
// Festgehalten, bevor überhaupt ein Versand versucht wurde.
|
||||
expect(InboundMail::query()->where('from_email', 'anna@example.at')->exists())->toBeTrue();
|
||||
});
|
||||
|
||||
it('gibt zwei Anfragen zwei verschiedene Kennungen', function () {
|
||||
// message_id ist eindeutig. Eine zweite Anfrage darf nicht an der
|
||||
// Datenbank scheitern, weil beide dieselbe synthetische Kennung tragen.
|
||||
foreach (['eins@example.at', 'zwei@example.at'] as $adresse) {
|
||||
$this->post(route('contact.send'), [
|
||||
'name' => 'Anna', 'email' => $adresse, 'message' => 'Eine ganz normale Anfrage.',
|
||||
])->assertRedirect();
|
||||
}
|
||||
|
||||
expect(InboundMail::query()->count())->toBe(2);
|
||||
});
|
||||
|
||||
it('trägt fremdes Markup nicht in die Mail, die unsere Adresse als Absender hat', function () {
|
||||
$this->post(route('contact.send'), [
|
||||
'name' => 'Anna Muster',
|
||||
|
|
|
|||
Loading…
Reference in New Issue