Add reply to mail to prevention and events
continuous-integration/drone/push Build is passing Details

This commit is contained in:
philipp lang 2025-11-07 14:34:26 +01:00
parent 28b47a2910
commit 28a4e70929
11 changed files with 75 additions and 4 deletions

View File

@ -2,7 +2,6 @@
namespace App\Form; namespace App\Form;
use App\Form\Actions\SettingStoreAction;
use App\Setting\Contracts\Storeable; use App\Setting\Contracts\Storeable;
use App\Setting\LocalSettings; use App\Setting\LocalSettings;
use Lorisleiva\Actions\ActionRequest; use Lorisleiva\Actions\ActionRequest;
@ -11,6 +10,7 @@ class FormSettings extends LocalSettings implements Storeable
{ {
public string $registerUrl; public string $registerUrl;
public string $clearCacheUrl; public string $clearCacheUrl;
public ?string $replyToMail;
public static function group(): string public static function group(): string
{ {
@ -19,7 +19,7 @@ class FormSettings extends LocalSettings implements Storeable
public static function title(): string public static function title(): string
{ {
return 'Formulare'; return 'Veranstaltungen';
} }
/** /**
@ -30,6 +30,7 @@ class FormSettings extends LocalSettings implements Storeable
return [ return [
'registerUrl' => 'present|string', 'registerUrl' => 'present|string',
'clearCacheUrl' => 'present|string', 'clearCacheUrl' => 'present|string',
'replyToMail' => 'nullable|string|email',
]; ];
} }
@ -47,6 +48,7 @@ class FormSettings extends LocalSettings implements Storeable
'data' => [ 'data' => [
'registerUrl' => $this->registerUrl, 'registerUrl' => $this->registerUrl,
'clearCacheUrl' => $this->clearCacheUrl, 'clearCacheUrl' => $this->clearCacheUrl,
'replyToMail' => $this->replyToMail,
] ]
] ]
]; ];

View File

@ -4,6 +4,7 @@ namespace App\Form\Mails;
use App\Form\Data\FormConfigData; use App\Form\Data\FormConfigData;
use App\Form\Editor\FormConditionResolver; use App\Form\Editor\FormConditionResolver;
use App\Form\FormSettings;
use App\Form\Models\Participant; use App\Form\Models\Participant;
use App\Lib\Editor\Condition; use App\Lib\Editor\Condition;
use Illuminate\Bus\Queueable; use Illuminate\Bus\Queueable;
@ -24,6 +25,8 @@ class ConfirmRegistrationMail extends Mailable
/** @var array<int, mixed> */ /** @var array<int, mixed> */
public array $bottomText; public array $bottomText;
public FormSettings $formSettings;
/** /**
* Create a new message instance. * Create a new message instance.
* *
@ -32,6 +35,7 @@ class ConfirmRegistrationMail extends Mailable
public function __construct(public Participant $participant) public function __construct(public Participant $participant)
{ {
$conditionResolver = app(FormConditionResolver::class)->forParticipant($participant); $conditionResolver = app(FormConditionResolver::class)->forParticipant($participant);
$this->formSettings = app(FormSettings::class);
$this->fullname = $participant->getFields()->getFullname(); $this->fullname = $participant->getFields()->getFullname();
$this->config = $participant->getConfig(); $this->config = $participant->getConfig();
$this->topText = $conditionResolver->makeBlocks($participant->form->mail_top); $this->topText = $conditionResolver->makeBlocks($participant->form->mail_top);
@ -45,9 +49,15 @@ class ConfirmRegistrationMail extends Mailable
*/ */
public function envelope() public function envelope()
{ {
return new Envelope( $envelope = new Envelope(
subject: 'Deine Anmeldung zu ' . $this->participant->form->name, subject: 'Deine Anmeldung zu ' . $this->participant->form->name,
); );
if ($this->formSettings->replyToMail !== null) {
$envelope->replyTo($this->formSettings->replyToMail);
}
return $envelope;
} }
/** /**

View File

@ -24,6 +24,7 @@ class SettingStoreAction
'weeks' => 'required|numeric|gte:0', 'weeks' => 'required|numeric|gte:0',
'freshRememberInterval' => 'required|numeric|gte:0', 'freshRememberInterval' => 'required|numeric|gte:0',
'active' => 'boolean', 'active' => 'boolean',
'replyToMail' => 'nullable|string|email',
]; ];
} }
@ -33,6 +34,7 @@ class SettingStoreAction
$settings->formmail = EditorData::from($request->formmail); $settings->formmail = EditorData::from($request->formmail);
$settings->yearlymail = EditorData::from($request->yearlymail); $settings->yearlymail = EditorData::from($request->yearlymail);
$settings->weeks = $request->weeks; $settings->weeks = $request->weeks;
$settings->replyToMail = $request->replyToMail;
$settings->freshRememberInterval = $request->freshRememberInterval; $settings->freshRememberInterval = $request->freshRememberInterval;
$settings->active = $request->active; $settings->active = $request->active;
$settings->yearlyMemberFilter = FilterScope::from($request->yearlyMemberFilter); $settings->yearlyMemberFilter = FilterScope::from($request->yearlyMemberFilter);

View File

@ -6,6 +6,7 @@ use App\Invoice\InvoiceSettings;
use App\Lib\Editor\EditorData; use App\Lib\Editor\EditorData;
use App\Prevention\Contracts\Preventable; use App\Prevention\Contracts\Preventable;
use App\Prevention\Data\PreventionData; use App\Prevention\Data\PreventionData;
use App\Prevention\PreventionSettings;
use Illuminate\Bus\Queueable; use Illuminate\Bus\Queueable;
use Illuminate\Mail\Attachment; use Illuminate\Mail\Attachment;
use Illuminate\Mail\Mailable; use Illuminate\Mail\Mailable;
@ -19,6 +20,7 @@ class YearlyMail extends Mailable
use Queueable, SerializesModels; use Queueable, SerializesModels;
public InvoiceSettings $settings; public InvoiceSettings $settings;
public PreventionSettings $preventionSettings;
/** /**
* Create a new message instance. * Create a new message instance.
@ -27,6 +29,7 @@ class YearlyMail extends Mailable
public function __construct(public Preventable $preventable, public EditorData $bodyText, public Collection $preventions) public function __construct(public Preventable $preventable, public EditorData $bodyText, public Collection $preventions)
{ {
$this->settings = app(InvoiceSettings::class); $this->settings = app(InvoiceSettings::class);
$this->preventionSettings = app(PreventionSettings::class);
$this->bodyText = $this->bodyText $this->bodyText = $this->bodyText
->replaceWithList('wanted', $preventions->map(fn($prevention) => $prevention->text())->toArray()); ->replaceWithList('wanted', $preventions->map(fn($prevention) => $prevention->text())->toArray());
} }
@ -38,9 +41,15 @@ class YearlyMail extends Mailable
*/ */
public function envelope() public function envelope()
{ {
return (new Envelope( $envelope = (new Envelope(
subject: $this->preventable->preventableSubject(), subject: $this->preventable->preventableSubject(),
))->to($this->preventable->getMailRecipient()->email, $this->preventable->getMailRecipient()->name); ))->to($this->preventable->getMailRecipient()->email, $this->preventable->getMailRecipient()->name);
if ($this->preventionSettings->replyToMail !== null) {
$envelope->replyTo($this->preventionSettings->replyToMail);
}
return $envelope;
} }
/** /**

View File

@ -15,6 +15,7 @@ class PreventionSettings extends LocalSettings
public int $freshRememberInterval; public int $freshRememberInterval;
public bool $active; public bool $active;
public FilterScope $yearlyMemberFilter; public FilterScope $yearlyMemberFilter;
public ?string $replyToMail;
/** /**
* @var array<int, string> * @var array<int, string>
* @todo Create collection cast to Collection of enums * @todo Create collection cast to Collection of enums
@ -49,6 +50,7 @@ class PreventionSettings extends LocalSettings
...$this->toArray(), ...$this->toArray(),
'weeks' => (string) $this->weeks, 'weeks' => (string) $this->weeks,
'freshRememberInterval' => (string) $this->freshRememberInterval, 'freshRememberInterval' => (string) $this->freshRememberInterval,
'replyToMail' => $this->replyToMail,
]; ];
} }
} }

View File

@ -0,0 +1,12 @@
<?php
use Spatie\LaravelSettings\Migrations\SettingsMigration;
return new class extends SettingsMigration
{
public function up(): void
{
$this->migrator->add('form.replyToMail', '');
$this->migrator->add('prevention.replyToMail', '');
}
};

View File

@ -11,6 +11,7 @@
<div class="grid grid-cols-2 gap-4"> <div class="grid grid-cols-2 gap-4">
<f-text id="register_url" v-model="inner.registerUrl" label="Formular-Link"></f-text> <f-text id="register_url" v-model="inner.registerUrl" label="Formular-Link"></f-text>
<f-text id="clear_cache_url" v-model="inner.clearCacheUrl" label="Frontend-Cache-Url"></f-text> <f-text id="clear_cache_url" v-model="inner.clearCacheUrl" label="Frontend-Cache-Url"></f-text>
<f-text id="reply_to_mail" v-model="inner.replyToMail" label="Reply-To-Adresse"></f-text>
</div> </div>
</form> </form>
</setting-layout> </setting-layout>

View File

@ -22,6 +22,7 @@
<f-editor v-if="active === 1" id="yearlymail" v-model="data.yearlymail" label="Jährliche Präventions-Erinnerung"></f-editor> <f-editor v-if="active === 1" id="yearlymail" v-model="data.yearlymail" label="Jährliche Präventions-Erinnerung"></f-editor>
<f-member-filter id="yearly_member_filter" v-model="data.yearlyMemberFilter" label="nur für folgende Mitglieder erlauben" /> <f-member-filter id="yearly_member_filter" v-model="data.yearlyMemberFilter" label="nur für folgende Mitglieder erlauben" />
<f-multipleselect id="prevent_against" v-model="data.preventAgainst" :options="meta.preventAgainsts" label="An diese Dokumente erinnern" size="sm"></f-multipleselect> <f-multipleselect id="prevent_against" v-model="data.preventAgainst" :options="meta.preventAgainsts" label="An diese Dokumente erinnern" size="sm"></f-multipleselect>
<f-text id="reply_to_mail" v-model="data.replyToMail" label="Reply-To-Adresse"></f-text>
</div> </div>
</form> </form>
</setting-layout> </setting-layout>

View File

@ -248,6 +248,17 @@ it('notices a few weeks before', function ($date, bool $shouldSend) {
[fn() => now()->subYears(5)->addWeeks(2)->subDay(), false], [fn() => now()->subYears(5)->addWeeks(2)->subDay(), false],
]); ]);
it('sets reply to mail', function () {
Mail::fake();
app(PreventionSettings::class)->fill(['replyToMail' => 'admin@example.com'])->save();
createMember(['has_vk' => false]);
sleep(2);
YearlyRememberAction::run();
Mail::assertSent(YearlyMail::class, fn ($message) => $message->hasReplyTo('admin@example.com'));
});
it('remembers members yearly', function ($date, $shouldSend) { it('remembers members yearly', function ($date, $shouldSend) {
Mail::fake(); Mail::fake();
createMember(['efz' => $date, 'ps_at' => now(), 'has_vk' => true]); createMember(['efz' => $date, 'ps_at' => now(), 'has_vk' => true]);

View File

@ -4,6 +4,7 @@ namespace Tests\Feature\Form;
use App\Form\Enums\NamiType; use App\Form\Enums\NamiType;
use App\Form\Enums\SpecialType; use App\Form\Enums\SpecialType;
use App\Form\FormSettings;
use App\Form\Mails\ConfirmRegistrationMail; use App\Form\Mails\ConfirmRegistrationMail;
use App\Form\Models\Form; use App\Form\Models\Form;
use App\Group; use App\Group;
@ -308,6 +309,22 @@ it('testItSendsEmailToParticipant', function () {
Mail::assertQueued(ConfirmRegistrationMail::class, fn($message) => $message->hasTo('example@test.test', 'Lala GG') && $message->hasSubject('Deine Anmeldung zu Ver2')); Mail::assertQueued(ConfirmRegistrationMail::class, fn($message) => $message->hasTo('example@test.test', 'Lala GG') && $message->hasSubject('Deine Anmeldung zu Ver2'));
}); });
it('sets reply to in email', function () {
$this->login()->loginNami()->withoutExceptionHandling();
app(FormSettings::class)->fill(['replyToMail' => 'reply@example.com'])->save();
$form = Form::factory()->name('Ver2')->fields([
$this->textField('vorname')->specialType(SpecialType::FIRSTNAME),
$this->textField('nachname')->specialType(SpecialType::LASTNAME),
$this->textField('email')->specialType(SpecialType::EMAIL),
])
->create();
$this->register($form, ['vorname' => 'Lala', 'nachname' => 'GG', 'email' => 'example@test.test'])
->assertOk();
Mail::assertQueued(ConfirmRegistrationMail::class, fn($message) => $message->hasReplyTo('reply@example.com'));
});
it('testItDoesntSendEmailWhenNoMailFieldGiven', function () { it('testItDoesntSendEmailWhenNoMailFieldGiven', function () {
$this->login()->loginNami()->withoutExceptionHandling(); $this->login()->loginNami()->withoutExceptionHandling();
$form = Form::factory()->fields([ $form = Form::factory()->fields([

View File

@ -28,6 +28,7 @@ it('receives settings', function () {
'weeks' => 9, 'weeks' => 9,
'freshRememberInterval' => 11, 'freshRememberInterval' => 11,
'active' => true, 'active' => true,
'replyToMail' => 'admin@example.com',
'preventAgainst' => [Prevention::MOREPS->name], 'preventAgainst' => [Prevention::MOREPS->name],
'yearlyMemberFilter' => FilterScope::from([ 'yearlyMemberFilter' => FilterScope::from([
'memberships' => [['group_ids' => [33]]], 'memberships' => [['group_ids' => [33]]],
@ -41,6 +42,7 @@ it('receives settings', function () {
->assertJsonPath('data.weeks', '9') ->assertJsonPath('data.weeks', '9')
->assertJsonPath('data.active', true) ->assertJsonPath('data.active', true)
->assertJsonPath('data.freshRememberInterval', '11') ->assertJsonPath('data.freshRememberInterval', '11')
->assertJsonPath('data.replyToMail', 'admin@example.com')
->assertJsonPath('data.yearlyMemberFilter.search', 'searchstring') ->assertJsonPath('data.yearlyMemberFilter.search', 'searchstring')
->assertJsonPath('data.yearlyMemberFilter.memberships.0.group_ids.0', 33) ->assertJsonPath('data.yearlyMemberFilter.memberships.0.group_ids.0', 33)
->assertJsonPath('data.preventAgainst', ['MOREPS']) ->assertJsonPath('data.preventAgainst', ['MOREPS'])
@ -58,6 +60,7 @@ it('testItStoresSettings', function () {
'freshRememberInterval' => 11, 'freshRememberInterval' => 11,
'active' => true, 'active' => true,
'preventAgainst' => ['EFZ'], 'preventAgainst' => ['EFZ'],
'replyToMail' => 'admin@example.com',
'yearlyMemberFilter' => [ 'yearlyMemberFilter' => [
'memberships' => [['group_ids' => 33]], 'memberships' => [['group_ids' => 33]],
'search' => 'searchstring', 'search' => 'searchstring',
@ -66,6 +69,7 @@ it('testItStoresSettings', function () {
test()->assertTrue(app(PreventionSettings::class)->formmail->hasAll(['new lorem'])); test()->assertTrue(app(PreventionSettings::class)->formmail->hasAll(['new lorem']));
test()->assertTrue(app(PreventionSettings::class)->yearlymail->hasAll(['lala dd'])); test()->assertTrue(app(PreventionSettings::class)->yearlymail->hasAll(['lala dd']));
test()->assertEquals(9, app(PreventionSettings::class)->weeks); test()->assertEquals(9, app(PreventionSettings::class)->weeks);
test()->assertEquals('admin@example.com', app(PreventionSettings::class)->replyToMail);
test()->assertEquals(11, app(PreventionSettings::class)->freshRememberInterval); test()->assertEquals(11, app(PreventionSettings::class)->freshRememberInterval);
test()->assertTrue(app(PreventionSettings::class)->active); test()->assertTrue(app(PreventionSettings::class)->active);
test()->assertEquals([['group_ids' => 33]], app(PreventionSettings::class)->yearlyMemberFilter->memberships); test()->assertEquals([['group_ids' => 33]], app(PreventionSettings::class)->yearlyMemberFilter->memberships);