Add prevention
This commit is contained in:
parent
8ec4e4eb59
commit
ee02b8df3a
|
@ -3,6 +3,7 @@
|
|||
namespace App\Console;
|
||||
|
||||
use App\Actions\DbMaintainAction;
|
||||
use App\Form\Actions\PreventionRememberAction;
|
||||
use App\Initialize\InitializeMembers;
|
||||
use App\Invoice\Actions\InvoiceSendAction;
|
||||
use Illuminate\Console\Scheduling\Schedule;
|
||||
|
@ -19,6 +20,7 @@ class Kernel extends ConsoleKernel
|
|||
InvoiceSendAction::class,
|
||||
InitializeMembers::class,
|
||||
DbMaintainAction::class,
|
||||
PreventionRememberAction::class,
|
||||
];
|
||||
|
||||
/**
|
||||
|
@ -30,6 +32,7 @@ class Kernel extends ConsoleKernel
|
|||
{
|
||||
$schedule->command(DbMaintainAction::class)->daily();
|
||||
$schedule->command(InitializeMembers::class)->dailyAt('03:00');
|
||||
$schedule->command(PreventionRememberAction::class)->dailyAt('11:00');
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -35,6 +35,7 @@ class FormStoreAction
|
|||
'is_active' => 'boolean',
|
||||
'is_private' => 'boolean',
|
||||
'export' => 'nullable|array',
|
||||
'needs_prevention' => 'present|boolean',
|
||||
];
|
||||
}
|
||||
|
||||
|
|
|
@ -34,6 +34,7 @@ class FormUpdateAction
|
|||
'is_active' => 'boolean',
|
||||
'is_private' => 'boolean',
|
||||
'export' => 'nullable|array',
|
||||
'needs_prevention' => 'present|boolean',
|
||||
];
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1,34 @@
|
|||
<?php
|
||||
|
||||
namespace App\Form\Actions;
|
||||
|
||||
use App\Form\Models\Participant;
|
||||
use App\Prevention\Mails\PreventionRememberMail;
|
||||
use Illuminate\Support\Facades\Mail;
|
||||
use Lorisleiva\Actions\Concerns\AsAction;
|
||||
|
||||
class PreventionRememberAction
|
||||
{
|
||||
use AsAction;
|
||||
|
||||
public string $commandSignature = 'prevention:remember';
|
||||
|
||||
public function handle(): void
|
||||
{
|
||||
$query = Participant::whereHas('form', fn ($form) => $form->where('needs_prevention', true))
|
||||
->where(
|
||||
fn ($q) => $q
|
||||
->where('last_remembered_at', '<=', now()->subWeeks(2))
|
||||
->orWhereNull('last_remembered_at')
|
||||
);
|
||||
foreach ($query->get() as $participant) {
|
||||
if (count($participant->preventions()) === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
Mail::send(new PreventionRememberMail($participant));
|
||||
|
||||
$participant->update(['last_remembered_at' => now()]);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -35,6 +35,7 @@ class Form extends Model implements HasMedia
|
|||
'is_active' => 'boolean',
|
||||
'is_private' => 'boolean',
|
||||
'export' => ExportData::class,
|
||||
'needs_prevention' => 'boolean',
|
||||
];
|
||||
|
||||
/** @var array<int, string> */
|
||||
|
|
|
@ -6,14 +6,17 @@ use App\Form\Data\FieldCollection;
|
|||
use App\Form\Data\FormConfigData;
|
||||
use App\Form\Mails\ConfirmRegistrationMail;
|
||||
use App\Form\Scopes\ParticipantFilterScope;
|
||||
use App\Member\Member;
|
||||
use App\Prevention\Contracts\Preventable;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||
use Illuminate\Support\Facades\Mail;
|
||||
use stdClass;
|
||||
|
||||
class Participant extends Model
|
||||
class Participant extends Model implements Preventable
|
||||
{
|
||||
use HasFactory;
|
||||
|
||||
|
@ -21,6 +24,7 @@ class Participant extends Model
|
|||
|
||||
public $casts = [
|
||||
'data' => 'json',
|
||||
'last_remembered_at' => 'datetime',
|
||||
];
|
||||
|
||||
/**
|
||||
|
@ -48,6 +52,11 @@ class Participant extends Model
|
|||
return $filter->apply($query);
|
||||
}
|
||||
|
||||
public function member(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(Member::class);
|
||||
}
|
||||
|
||||
public function getFields(): FieldCollection
|
||||
{
|
||||
return FieldCollection::fromRequest($this->form, $this->data);
|
||||
|
@ -70,6 +79,29 @@ class Participant extends Model
|
|||
return;
|
||||
}
|
||||
|
||||
Mail::to($this->getFields()->getMailRecipient())->queue(new ConfirmRegistrationMail($this));
|
||||
Mail::to($this->getMailRecipient())->queue(new ConfirmRegistrationMail($this));
|
||||
}
|
||||
|
||||
public function preventableLayout(): string
|
||||
{
|
||||
return 'mail.prevention.prevention-remember-participant';
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
public function preventions(): array
|
||||
{
|
||||
return $this->member?->preventions($this->form->from) ?: [];
|
||||
}
|
||||
|
||||
public function getMailRecipient(): stdClass
|
||||
{
|
||||
return $this->getFields()->getMailRecipient();
|
||||
}
|
||||
|
||||
public function preventableSubject(): string
|
||||
{
|
||||
return 'Nachweise erforderlich für deine Anmeldung zu ' . $this->form->name;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -49,6 +49,7 @@ class FormResource extends JsonResource
|
|||
'is_private' => $this->is_private,
|
||||
'has_nami_field' => $this->getFields()->hasNamiField(),
|
||||
'export' => $this->export,
|
||||
'needs_prevention' => $this->needs_prevention,
|
||||
'links' => [
|
||||
'participant_index' => route('form.participant.index', ['form' => $this->getModel(), 'parent' => null]),
|
||||
'participant_root_index' => route('form.participant.index', ['form' => $this->getModel(), 'parent' => -1]),
|
||||
|
|
|
@ -23,7 +23,6 @@ use Illuminate\Database\Eloquent\Model;
|
|||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||
use Illuminate\Notifications\Notifiable;
|
||||
use Laravel\Scout\Attributes\SearchUsingFullText;
|
||||
use Laravel\Scout\Searchable;
|
||||
use Sabre\VObject\Component\VCard;
|
||||
use Sabre\VObject\Reader;
|
||||
|
@ -33,6 +32,7 @@ use Zoomyboy\Osm\Coordinate;
|
|||
use Zoomyboy\Osm\Geolocatable;
|
||||
use Zoomyboy\Osm\HasGeolocation;
|
||||
use Zoomyboy\Phone\HasPhoneNumbers;
|
||||
use App\Prevention\Enums\Prevention;
|
||||
|
||||
/**
|
||||
* @property string $subscription_name
|
||||
|
@ -341,6 +341,37 @@ class Member extends Model implements Geolocatable
|
|||
return $query->where('bill_kind', '!=', null)->where('subscription_id', '!=', null);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<int, Prevention>
|
||||
*/
|
||||
public function preventions(?Carbon $date = null): array
|
||||
{
|
||||
$date = $date ?: now();
|
||||
if (!$this->isLeader()) {
|
||||
return [];
|
||||
}
|
||||
|
||||
/** @var array<int, Prevention */
|
||||
$preventions = [];
|
||||
|
||||
if ($this->efz === null || $this->efz->diffInYears($date) >= 5) {
|
||||
$preventions[] = Prevention::EFZ;
|
||||
}
|
||||
|
||||
if ($this->more_ps_at === null) {
|
||||
if ($this->ps_at === null || $this->ps_at->diffInYears($date) >= 5) {
|
||||
$preventions[] = Prevention::PS;
|
||||
}
|
||||
} else {
|
||||
if ($this->more_ps_at === null || $this->more_ps_at->diffInYears($date) >= 5) {
|
||||
$preventions[] = Prevention::MOREPS;
|
||||
}
|
||||
}
|
||||
|
||||
return $preventions;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @param Builder<self> $query
|
||||
*
|
||||
|
|
|
@ -0,0 +1,20 @@
|
|||
<?php
|
||||
|
||||
namespace App\Prevention\Contracts;
|
||||
|
||||
use App\Prevention\Enums\Prevention;
|
||||
use stdClass;
|
||||
|
||||
interface Preventable
|
||||
{
|
||||
|
||||
public function preventableLayout(): string;
|
||||
public function preventableSubject(): string;
|
||||
|
||||
/**
|
||||
* @return array<int, Prevention>
|
||||
*/
|
||||
public function preventions(): array;
|
||||
|
||||
public function getMailRecipient(): stdClass;
|
||||
}
|
|
@ -0,0 +1,19 @@
|
|||
<?php
|
||||
|
||||
namespace App\Prevention\Enums;
|
||||
|
||||
enum Prevention
|
||||
{
|
||||
case EFZ;
|
||||
case PS;
|
||||
case MOREPS;
|
||||
|
||||
public function text(): string
|
||||
{
|
||||
return match ($this) {
|
||||
static::EFZ => 'erweitertes Führungszeugnis',
|
||||
static::PS => 'Präventionsschulung Basis Plus',
|
||||
static::MOREPS => 'Präventionsschulung (Auffrischung)',
|
||||
};
|
||||
}
|
||||
}
|
|
@ -0,0 +1,62 @@
|
|||
<?php
|
||||
|
||||
namespace App\Prevention\Mails;
|
||||
|
||||
use App\Invoice\InvoiceSettings;
|
||||
use App\Prevention\Contracts\Preventable;
|
||||
use Illuminate\Bus\Queueable;
|
||||
use Illuminate\Mail\Mailable;
|
||||
use Illuminate\Mail\Mailables\Content;
|
||||
use Illuminate\Mail\Mailables\Envelope;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
|
||||
class PreventionRememberMail extends Mailable
|
||||
{
|
||||
use Queueable, SerializesModels;
|
||||
|
||||
public InvoiceSettings $settings;
|
||||
public string $documents;
|
||||
|
||||
/**
|
||||
* Create a new message instance.
|
||||
*/
|
||||
public function __construct(public Preventable $preventable)
|
||||
{
|
||||
$this->settings = app(InvoiceSettings::class);
|
||||
$this->documents = collect($preventable->preventions())->map(fn ($prevention) => "* {$prevention->text()}")->implode("\n");
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the message envelope.
|
||||
*
|
||||
* @return \Illuminate\Mail\Mailables\Envelope
|
||||
*/
|
||||
public function envelope()
|
||||
{
|
||||
return (new Envelope(
|
||||
subject: $this->preventable->preventableSubject(),
|
||||
))->to($this->preventable->getMailRecipient()->email, $this->preventable->getMailRecipient()->name);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the message content definition.
|
||||
*
|
||||
* @return \Illuminate\Mail\Mailables\Content
|
||||
*/
|
||||
public function content()
|
||||
{
|
||||
return new Content(
|
||||
markdown: $this->preventable->preventableLayout(),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the attachments for the message.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function attachments()
|
||||
{
|
||||
return [];
|
||||
}
|
||||
}
|
|
@ -0,0 +1,35 @@
|
|||
<?php
|
||||
|
||||
use App\Form\Models\Participant;
|
||||
use App\Member\Member;
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function up()
|
||||
{
|
||||
Schema::table('participants', function (Blueprint $table) {
|
||||
$table->datetime('last_remembered_at')->nullable();
|
||||
});
|
||||
|
||||
Schema::table('forms', function (Blueprint $table) {
|
||||
$table->boolean('needs_prevention')->default(false);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function down()
|
||||
{
|
||||
}
|
||||
};
|
|
@ -43,6 +43,7 @@
|
|||
<f-text id="name" v-model="single.name" class="grow" label="Name" required></f-text>
|
||||
<f-switch id="is_active" v-model="single.is_active" name="is_active" label="Aktiv"></f-switch>
|
||||
<f-switch id="is_private" v-model="single.is_private" name="is_private" label="Privat"></f-switch>
|
||||
<f-switch id="needs_prevention" v-model="single.needs_prevention" name="needs_prevention" label="Prävention"></f-switch>
|
||||
</div>
|
||||
<f-singlefile
|
||||
id="header_image"
|
||||
|
|
|
@ -0,0 +1,17 @@
|
|||
@component('mail::message')
|
||||
# Hallo {{ $preventable->member->fullname }},
|
||||
|
||||
Du hast dich für die Veranstaltung __{{$preventable->form->name}}__ angemeldet.
|
||||
|
||||
Damit du an der Veranstaltung als leitende oder helfende Person teilnehmen kannst, ist noch folgendes einzureichen oder zu beachten.
|
||||
|
||||
{!! $documents !!}
|
||||
|
||||
@component('mail::subcopy')
|
||||
|
||||
Herzliche Grüße und gut Pfad
|
||||
|
||||
{{$settings->from_long}}
|
||||
@endcomponent
|
||||
|
||||
@endcomponent
|
|
@ -52,6 +52,7 @@ class FormIndexActionTest extends FormTestCase
|
|||
->assertInertiaPath('data.data.0.is_active', true)
|
||||
->assertInertiaPath('data.data.0.is_private', false)
|
||||
->assertInertiaPath('data.data.0.registration_from', '2023-05-06 04:00:00')
|
||||
->assertInertiaPath('data.data.0.needs_prevention', false)
|
||||
->assertInertiaPath('data.data.0.registration_until', '2023-04-01 05:00:00')
|
||||
->assertInertiaPath('data.data.0.links.participant_index', route('form.participant.index', ['form' => $form]))
|
||||
->assertInertiaPath('data.data.0.links.export', route('form.export', ['form' => $form]))
|
||||
|
|
|
@ -49,6 +49,7 @@ class FormRequest extends RequestFactory
|
|||
'header_image' => $this->getHeaderImagePayload(str()->uuid() . '.jpg'),
|
||||
'mailattachments' => [],
|
||||
'export' => ExportData::from([])->toArray(),
|
||||
'needs_prevention' => $this->faker->boolean(),
|
||||
];
|
||||
}
|
||||
|
||||
|
|
|
@ -6,7 +6,6 @@ use App\Fileshare\Data\FileshareResourceData;
|
|||
use App\Form\Data\ExportData;
|
||||
use App\Form\Models\Form;
|
||||
use Illuminate\Foundation\Testing\DatabaseTransactions;
|
||||
use Illuminate\Support\Facades\Http;
|
||||
|
||||
class FormUpdateActionTest extends FormTestCase
|
||||
{
|
||||
|
@ -124,4 +123,14 @@ class FormUpdateActionTest extends FormTestCase
|
|||
$this->patchJson(route('form.update', ['form' => $form]), $payload)->assertSessionDoesntHaveErrors()->assertOk();
|
||||
$this->assertEquals(['firstname', 'geb', 'lastname'], $form->fresh()->meta['active_columns']);
|
||||
}
|
||||
|
||||
public function testItUpdatesPrevention(): void
|
||||
{
|
||||
$this->login()->loginNami()->withoutExceptionHandling();
|
||||
$form = Form::factory()->create();
|
||||
$payload = FormRequest::new()->state(['needs_prevention' => true])->create();
|
||||
|
||||
$this->patchJson(route('form.update', ['form' => $form]), $payload);
|
||||
$this->assertTrue($form->fresh()->needs_prevention);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,183 @@
|
|||
<?php
|
||||
|
||||
namespace Tests\Feature\Member;
|
||||
|
||||
use App\Prevention\Enums\Prevention;
|
||||
use App\Form\Actions\PreventionRememberAction;
|
||||
use App\Form\Enums\NamiType;
|
||||
use App\Form\Models\Form;
|
||||
use App\Form\Models\Participant;
|
||||
use App\Invoice\InvoiceSettings;
|
||||
use App\Prevention\Mails\PreventionRememberMail;
|
||||
use App\Member\Member;
|
||||
use App\Member\Membership;
|
||||
use Generator;
|
||||
use Illuminate\Foundation\Testing\DatabaseTransactions;
|
||||
use Illuminate\Support\Facades\Mail;
|
||||
use Tests\Lib\CreatesFormFields;
|
||||
use Tests\TestCase;
|
||||
|
||||
class PreventionTest extends TestCase
|
||||
{
|
||||
|
||||
use DatabaseTransactions;
|
||||
use CreatesFormFields;
|
||||
|
||||
public function testItRemembersWhenNotRememberedYet(): void
|
||||
{
|
||||
Mail::fake();
|
||||
$form = $this->createForm();
|
||||
$participant = $this->createParticipant($form);
|
||||
|
||||
PreventionRememberAction::run();
|
||||
|
||||
$this->assertEquals(now()->format('Y-m-d'), $participant->fresh()->last_remembered_at->format('Y-m-d'));
|
||||
}
|
||||
|
||||
public function testItRemembersWhenRememberIsDue(): void
|
||||
{
|
||||
Mail::fake();
|
||||
$form = $this->createForm();
|
||||
$participant = tap($this->createParticipant($form), fn ($p) => $p->update(['last_remembered_at' => now()->subWeeks(3)]));
|
||||
|
||||
PreventionRememberAction::run();
|
||||
|
||||
$this->assertEquals(now()->format('Y-m-d'), $participant->fresh()->last_remembered_at->format('Y-m-d'));
|
||||
}
|
||||
|
||||
public function testItDoesntRememberWhenRememberingIsNotDue(): void
|
||||
{
|
||||
Mail::fake();
|
||||
$form = $this->createForm();
|
||||
$participant = tap($this->createParticipant($form), fn ($p) => $p->update(['last_remembered_at' => now()->subWeeks(1)]));
|
||||
|
||||
PreventionRememberAction::run();
|
||||
|
||||
$this->assertEquals(now()->subWeeks(1)->format('Y-m-d'), $participant->fresh()->last_remembered_at->format('Y-m-d'));
|
||||
}
|
||||
|
||||
public function testItDoesntRememberWhenFormDoesntNeedPrevention(): void
|
||||
{
|
||||
Mail::fake();
|
||||
$form = tap($this->createForm(), fn ($form) => $form->update(['needs_prevention' => false]));
|
||||
$participant = $this->createParticipant($form);
|
||||
|
||||
PreventionRememberAction::run();
|
||||
|
||||
$this->assertNull($participant->fresh()->last_remembered_at);
|
||||
}
|
||||
|
||||
public function testItDoesntRememberWhenParticipantDoesntHaveMember(): void
|
||||
{
|
||||
Mail::fake();
|
||||
$form = $this->createForm();
|
||||
$participant = $this->createParticipant($form);
|
||||
$participant->member->delete();
|
||||
|
||||
PreventionRememberAction::run();
|
||||
|
||||
$this->assertNull($participant->fresh()->last_remembered_at);
|
||||
}
|
||||
|
||||
public function testItDoesntRememberWhenMemberIsNotALeader(): void
|
||||
{
|
||||
Mail::fake();
|
||||
$form = $this->createForm();
|
||||
$participant = $this->createParticipant($form);
|
||||
$participant->member->memberships->each->delete();
|
||||
|
||||
PreventionRememberAction::run();
|
||||
|
||||
$this->assertNull($participant->fresh()->last_remembered_at);
|
||||
}
|
||||
|
||||
private function attributes(): Generator
|
||||
{
|
||||
yield [
|
||||
'attrs' => ['efz' => null, 'ps_at' => now()],
|
||||
'preventions' => [Prevention::EFZ]
|
||||
];
|
||||
|
||||
yield [
|
||||
'attrs' => ['efz' => now(), 'ps_at' => null],
|
||||
'preventions' => [Prevention::PS]
|
||||
];
|
||||
|
||||
yield [
|
||||
'attrs' => ['efz' => now()->subDay(), 'ps_at' => now()],
|
||||
'preventions' => []
|
||||
];
|
||||
|
||||
yield [
|
||||
'attrs' => ['efz' => now(), 'ps_at' => now()->subDay()],
|
||||
'preventions' => []
|
||||
];
|
||||
|
||||
yield [
|
||||
'attrs' => ['efz' => now()->subYears(5)->subDay(), 'ps_at' => now()],
|
||||
'preventions' => [Prevention::EFZ]
|
||||
];
|
||||
|
||||
yield [
|
||||
'attrs' => ['efz' => now(), 'ps_at' => now()->subYears(5)->subDay()],
|
||||
'preventions' => [Prevention::PS]
|
||||
];
|
||||
|
||||
yield [
|
||||
'attrs' => ['efz' => now(), 'ps_at' => now()->subYears(5)->subDay(), 'more_ps_at' => now()],
|
||||
'preventions' => []
|
||||
];
|
||||
|
||||
yield [
|
||||
'attrs' => ['efz' => now(), 'ps_at' => now()->subYears(15), 'more_ps_at' => now()->subYears(5)->subDay()],
|
||||
'preventions' => [Prevention::MOREPS],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<int, Prevention> $preventions
|
||||
* @dataProvider attributes
|
||||
*/
|
||||
public function testItRemembersMember(array $memberAttributes, array $preventions): void
|
||||
{
|
||||
Mail::fake();
|
||||
$form = $this->createForm();
|
||||
$participant = $this->createParticipant($form);
|
||||
$participant->member->update($memberAttributes);
|
||||
|
||||
PreventionRememberAction::run();
|
||||
|
||||
if (count($preventions)) {
|
||||
Mail::assertSent(PreventionRememberMail::class, fn ($mail) => $mail->preventable->preventions() === $preventions);
|
||||
$this->assertNotNull($participant->fresh()->last_remembered_at);
|
||||
} else {
|
||||
Mail::assertNotSent(PreventionRememberMail::class);
|
||||
$this->assertNull($participant->fresh()->last_remembered_at);
|
||||
}
|
||||
}
|
||||
|
||||
public function testItRendersMail(): void
|
||||
{
|
||||
InvoiceSettings::fake(['from_long' => 'Stamm Beispiel']);
|
||||
$form = $this->createForm();
|
||||
$participant = $this->createParticipant($form);
|
||||
(new PreventionRememberMail($participant))
|
||||
->assertSeeInText($participant->member->firstname)
|
||||
->assertSeeInText($participant->form->name)
|
||||
->assertSeeInText('erweitertes Führungszeugnis')
|
||||
->assertSeeInText('Stamm Beispiel')
|
||||
->assertSeeInText($participant->member->lastname);
|
||||
}
|
||||
|
||||
protected function createForm(): Form
|
||||
{
|
||||
return Form::factory()->fields([
|
||||
$this->textField('vorname')->namiType(NamiType::FIRSTNAME),
|
||||
])->create(['needs_prevention' => true]);
|
||||
}
|
||||
|
||||
protected function createParticipant(Form $form): Participant
|
||||
{
|
||||
return Participant::factory()->for($form)->data(['vorname' => 'Max'])->for(Member::factory()->defaults()->has(Membership::factory()->inLocal('€ LeiterIn', 'Wölfling')))->create();
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue