feat: admin editable email templates

addresses #869eg7ptr
This commit is contained in:
Björn Fromme committed 2026-08-17 10:39:19 +02:00
1 parent 804dab335f
commit 3e08965e48
49 files changed
+1760 -276

No files matched your search

+75
View File
@@ -0,0 +1,75 @@
<?php
namespace App\Config;
use Symfony\Component\Yaml\Yaml;
/**
* Reads the delivered wording of the admin-editable mails from config/email_texts.yaml.
*
* That wording is the fallback used whenever no email_text row exists for a key, so the
* mails keep working on a fresh database and "reset to default" needs nothing but deleting
* the row.
*
* The file is parsed lazily and memoised: a request that sends no mail and opens no admin
* screen never touches it, and one that does reads it once. Nothing here is validated at
* compile time, so a missing entry surfaces as an error the first time the catalogue is
* built - EmailTextCatalogTest builds all of them.
*/
class EmailTextCatalog
{
/**
* @var array<string, EmailTextDefinition>|null
*/
private ?array $definitions = null;
public function __construct(
private readonly string $emailTextsFile,
) {
}
/**
* @return array<string, EmailTextDefinition> keyed by EmailTextKey value
*/
public function all(): array
{
return $this->definitions ??= $this->build();
}
public function get(EmailTextKey $key): EmailTextDefinition
{
return $this->all()[$key->value];
}
/**
* @return array<string, EmailTextDefinition>
*/
private function build(): array
{
$config = Yaml::parseFile($this->emailTextsFile);
$descriptions = $config['placeholders'];
$definitions = [];
foreach (EmailTextKey::cases() as $key) {
$text = $config['texts'][$key->value];
$placeholders = [];
foreach ($text['placeholders'] as $name) {
$placeholders[$name] = $descriptions[$name];
}
$definitions[$key->value] = new EmailTextDefinition(
$key,
$text['label'],
$placeholders,
$text['subject'],
$text['headline'],
$text['body'],
);
}
return $definitions;
}
}
+31
View File
@@ -0,0 +1,31 @@
<?php
namespace App\Config;
/**
* The parts of an editable mail that stay in code: its name in the admin UI, the
* placeholders it accepts, and the wording that applies until an admin overrides it.
*/
readonly class EmailTextDefinition
{
/**
* @param array<string, string> $placeholders name => description shown to the admin
*/
public function __construct(
public EmailTextKey $key,
public string $label,
public array $placeholders,
public string $defaultSubject,
public string $defaultHeadline,
public string $defaultBody,
) {
}
/**
* @return string[]
*/
public function getPlaceholderNames(): array
{
return array_keys($this->placeholders);
}
}
+21
View File
@@ -0,0 +1,21 @@
<?php
namespace App\Config;
/**
* The transactional mails whose wording admins may edit.
*
* A case's value is what ends up in email_text.template_key, so renaming one orphans
* the stored override and silently falls back to the default in EmailTextCatalog.
*/
enum EmailTextKey: string
{
case APPLICATION_ACCEPTED = 'application_accepted';
case APPLICATION_REJECTED = 'application_rejected';
case ASSIGNMENT_CALLED_OFF = 'assignment_called_off';
case DISPOSITION_CALLED_OFF = 'disposition_called_off';
case DOCUMENT_REJECTED = 'document_rejected';
case REMINDER_CONTRACT_UPLOAD = 'reminder_contract_upload';
case REMINDER_INVOICE_UPLOAD = 'reminder_invoice_upload';
case REMINDER_DISPOSITION = 'reminder_disposition';
}