From 88487f0d3991c2e7ae75b6dd0642f7a9f175a517 Mon Sep 17 00:00:00 2001 From: philipp lang Date: Mon, 23 Jun 2025 22:37:06 +0200 Subject: [PATCH] --wip-- [skip ci] --- .../Actions/GenerateContributionAction.php | 43 +++++++ app/Form/Data/FieldCollection.php | 7 +- app/Form/Enums/SpecialType.php | 6 + app/Form/Models/Form.php | 112 +++++++++++++++-- docker-compose.yml | 4 + tests/Datasets/contribution.php | 10 ++ .../Feature/Form/GenerateContributionTest.php | 116 ++++++++++++++++++ 7 files changed, 288 insertions(+), 10 deletions(-) create mode 100644 app/Form/Actions/GenerateContributionAction.php create mode 100644 tests/Feature/Form/GenerateContributionTest.php diff --git a/app/Form/Actions/GenerateContributionAction.php b/app/Form/Actions/GenerateContributionAction.php new file mode 100644 index 00000000..b06759ce --- /dev/null +++ b/app/Form/Actions/GenerateContributionAction.php @@ -0,0 +1,43 @@ +type()::fromPayload($request)); + } + + public function asController(ActionRequest $request, Form $form): BaseCompiler|JsonResponse + { + app(ContributionFactory::class)->validateType($form); + $form->validateContribution(); + + return $request->input('validate') + ? response()->json([]) + : $this->handle($form); + } + + /** + * @return array + */ + public function rules(): array + { + return [ + 'payload' => [new JsonBase64Rule()], + ]; + } +} diff --git a/app/Form/Data/FieldCollection.php b/app/Form/Data/FieldCollection.php index f1d0348b..0f989074 100644 --- a/app/Form/Data/FieldCollection.php +++ b/app/Form/Data/FieldCollection.php @@ -114,7 +114,12 @@ class FieldCollection extends Collection return $this->map(fn ($field) => $field->presentRaw())->toArray(); } - private function findBySpecialType(SpecialType $specialType): ?Field + public function hasSpecialType(SpecialType $specialType): bool + { + return $this->findBySpecialType($specialType) !== null; + } + + public function findBySpecialType(SpecialType $specialType): ?Field { return $this->first(fn ($field) => $field->specialType === $specialType); } diff --git a/app/Form/Enums/SpecialType.php b/app/Form/Enums/SpecialType.php index f136432e..88d54659 100644 --- a/app/Form/Enums/SpecialType.php +++ b/app/Form/Enums/SpecialType.php @@ -7,6 +7,12 @@ enum SpecialType: string case FIRSTNAME = 'Vorname'; case LASTNAME = 'Nachname'; case EMAIL = 'E-Mail-Adresse'; + case BIRTHDAY = 'Geburtsdatum'; + case ZIP = 'PLZ'; + case LOCATION = 'Ort'; + case ADDRESS = 'Adresse'; + case GENDER = 'Geschlecht'; + case LEADER = 'LeiterIn'; /** * @return array diff --git a/app/Form/Models/Form.php b/app/Form/Models/Form.php index ca2681b5..962863c1 100644 --- a/app/Form/Models/Form.php +++ b/app/Form/Models/Form.php @@ -2,18 +2,25 @@ namespace App\Form\Models; +use App\Contribution\Contracts\HasContributionData; +use App\Contribution\Data\MemberData; +use App\Country; use App\Form\Actions\UpdateParticipantSearchIndexAction; use App\Form\Data\ExportData; use App\Form\Data\FieldCollection; use App\Form\Data\FormConfigData; +use App\Form\Enums\SpecialType; use App\Lib\Editor\Condition; use App\Lib\Editor\EditorData; use App\Lib\Sorting; +use Carbon\Carbon; use Cviebrock\EloquentSluggable\Sluggable; use Database\Factories\Form\Models\FormFactory; use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Relations\HasMany; +use Illuminate\Support\Collection; +use Illuminate\Validation\ValidationException; use Laravel\Scout\Searchable; use Spatie\Image\Enums\Fit; use Spatie\MediaLibrary\HasMedia; @@ -22,7 +29,7 @@ use Spatie\MediaLibrary\MediaCollections\Models\Media; use Zoomyboy\MedialibraryHelper\DefersUploads; /** @todo replace editor content with EditorData cast */ -class Form extends Model implements HasMedia +class Form extends Model implements HasMedia, HasContributionData { /** @use HasFactory */ use HasFactory; @@ -74,20 +81,20 @@ class Form extends Model implements HasMedia { $this->addMediaCollection('headerImage') ->singleFile() - ->maxWidth(fn () => 500) - ->forceFileName(fn (Form $model, string $name) => $model->slug) - ->convert(fn () => 'jpg') + ->maxWidth(fn() => 500) + ->forceFileName(fn(Form $model, string $name) => $model->slug) + ->convert(fn() => 'jpg') ->registerMediaConversions(function (Media $media) { $this->addMediaConversion('square')->fit(Fit::Crop, 400, 400); }); $this->addMediaCollection('mailattachments') - ->withDefaultProperties(fn () => [ + ->withDefaultProperties(fn() => [ 'conditions' => [ 'mode' => 'all', 'ifs' => [] ], ]) - ->withPropertyValidation(fn () => [ + ->withPropertyValidation(fn() => [ 'conditions.mode' => 'required|string|in:all,any', 'conditions.ifs' => 'array', 'conditions.ifs.*.field' => 'required', @@ -101,7 +108,7 @@ class Form extends Model implements HasMedia */ public function getRegistrationRules(): array { - return $this->getFields()->reduce(fn ($carry, $field) => [ + return $this->getFields()->reduce(fn($carry, $field) => [ ...$carry, ...$field->getRegistrationRules($this), ], []); @@ -112,7 +119,7 @@ class Form extends Model implements HasMedia */ public function getRegistrationMessages(): array { - return $this->getFields()->reduce(fn ($carry, $field) => [ + return $this->getFields()->reduce(fn($carry, $field) => [ ...$carry, ...$field->getRegistrationMessages($this), ], []); @@ -123,7 +130,7 @@ class Form extends Model implements HasMedia */ public function getRegistrationAttributes(): array { - return $this->getFields()->reduce(fn ($carry, $field) => [ + return $this->getFields()->reduce(fn($carry, $field) => [ ...$carry, ...$field->getRegistrationAttributes($this), ], []); @@ -201,4 +208,91 @@ class Form extends Model implements HasMedia return true; } + + public function payload(): array + { + return [ + 'dateFrom' => $this->dateFrom()->format('Y-m-d'), + 'eventName' => $this->name, + 'members' => [], + ]; + } + + /** + * @return class-string + */ + public function type(): string + { + return request()->input('type'); + } + + public function dateFrom(): Carbon + { + return now(); + } + + public function dateUntil(): Carbon + { + return now(); + } + + public function zipLocation(): string + { + return ''; + } + + public function eventName(): string + { + return $this->name; + } + + public function members(): Collection + { + $members = []; + $fields = [ + [SpecialType::FIRSTNAME, 'firstname'], + [SpecialType::LASTNAME, 'lastname'], + [SpecialType::BIRTHDAY, 'birthday'], + [SpecialType::ADDRESS, 'address'], + [SpecialType::ZIP, 'zip'], + [SpecialType::LOCATION, 'location'] + ]; + + foreach ($this->participants as $participant) { + $member = []; + foreach ($fields as [$type, $name]) { + $f = $this->getFields()->findBySpecialType($type); + $member[$name] = $participant->getFields()->find($f)->value; + } + + $members[] = [ + 'is_leader' => false, + 'gender' => null, + ...$member, + ]; + } + + return MemberData::fromApi($members); + } + + public function country(): ?Country + { + return Country::first(); + } + + public function validateContribution(): void + { + $messages = []; + foreach ($this->type()::requiredFormSpecialTypes() as $type) { + if (!$this->getFields()->hasSpecialType($type)) { + $messages[$type->name] = 'Kein Feld für ' . $type->value . ' vorhanden.'; + } + } + + if ($this->participants->count() === 0) { + $messages['participants'] = 'Veranstaltung besitzt noch keine Teilnehmer*innen.'; + } + + throw_unless(empty($messages), ValidationException::withMessages($messages)); + } } diff --git a/docker-compose.yml b/docker-compose.yml index aa27c717..48522bec 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -92,6 +92,8 @@ services: socketi: image: quay.io/soketi/soketi:89604f268623cf799573178a7ba56b7491416bde-16-debian + ports: + - "6001:6001" environment: SOKETI_DEFAULT_APP_ID: adremaid SOKETI_DEFAULT_APP_KEY: adremakey @@ -104,6 +106,8 @@ services: meilisearch: image: getmeili/meilisearch:v1.6 + ports: + - "7700:7700" volumes: - ./data/meilisearch:/meili_data env_file: diff --git a/tests/Datasets/contribution.php b/tests/Datasets/contribution.php index 6d3f5043..f0af8774 100644 --- a/tests/Datasets/contribution.php +++ b/tests/Datasets/contribution.php @@ -100,3 +100,13 @@ dataset('contribution-assertions', fn () => [ [BdkjHesse::class, ["Max", "Muster", "Jane"]], [WuppertalDocument::class, ["Max", "Muster", "Jane", "42777 SG", "15.06.1991", "16.06.1991"]], ]); + +dataset('contribution-documents', fn () => [ + CitySolingenDocument::class, + RdpNrwDocument::class, + CityRemscheidDocument::class, + CityFrankfurtMainDocument::class, + BdkjHesse::class, + WuppertalDocument::class, +]); + diff --git a/tests/Feature/Form/GenerateContributionTest.php b/tests/Feature/Form/GenerateContributionTest.php new file mode 100644 index 00000000..710dc511 --- /dev/null +++ b/tests/Feature/Form/GenerateContributionTest.php @@ -0,0 +1,116 @@ + $fields + */ +it('doesnt create document when no special fields given', function (array $fields, string $field, string $message, string $type) { + $this->login()->loginNami(); + + $form = Form::factory() + ->fields($fields) + ->has(Participant::factory()) + ->create(); + + $this->json('GET', route('form.contribution', [ + 'type' => $type, + 'form' => $form, + 'validate' => '1', + ]))->assertJsonValidationErrors([$field => $message]); +}) +->with([ + [fn () => [], 'FIRSTNAME', 'Kein Feld für Vorname vorhanden.'], + [fn () => [test()->textField('f')->specialType(SpecialType::FIRSTNAME)], 'LASTNAME', 'Kein Feld für Nachname vorhanden.'], + [fn () => [test()->textField('f')->specialType(SpecialType::FIRSTNAME), test()->textField('l')->specialType(SpecialType::LASTNAME)], 'BIRTHDAY', 'Kein Feld für Geburtsdatum vorhanden.'], + [fn () => [test()->textField('f')->specialType(SpecialType::FIRSTNAME), test()->textField('l')->specialType(SpecialType::LASTNAME), test()->dateField('b')->specialType(SpecialType::BIRTHDAY)], 'ZIP', 'Kein Feld für PLZ vorhanden.'], + [fn () => [test()->textField('f')->specialType(SpecialType::FIRSTNAME), test()->textField('l')->specialType(SpecialType::LASTNAME), test()->dateField('b')->specialType(SpecialType::BIRTHDAY), test()->dateField('p')->specialType(SpecialType::ZIP)], 'LOCATION', 'Kein Feld für Ort vorhanden.'], +])->with('contribution-documents'); + +/** + * @param array $fields + */ +it('validates special types of each document', function (string $type, array $fields, string $field, string $message) { + $this->login()->loginNami(); + + $form = Form::factory()->fields([ + test()->textField('f')->specialType(SpecialType::FIRSTNAME), + test()->textField('l')->specialType(SpecialType::LASTNAME), + test()->dateField('b')->specialType(SpecialType::BIRTHDAY), + test()->dateField('p')->specialType(SpecialType::ZIP), + test()->dateField('l')->specialType(SpecialType::LOCATION), + ...$fields, + ]) + ->has(Participant::factory()) + ->create(); + + $this->json('GET', route('form.contribution', [ + 'type' => $type, + 'form' => $form, + 'validate' => '1', + ]))->assertJsonValidationErrors([$field => $message]); +}) +->with([ + [CitySolingenDocument::class, [], 'ADDRESS', 'Kein Feld für Adresse vorhanden.'], + [RdpNrwDocument::class, [], 'GENDER', 'Kein Feld für Geschlecht vorhanden.'], +]); + +it('throws error when not validating but fields are not present', function () { + $this->login()->loginNami(); + + $form = Form::factory()->fields([]) + ->has(Participant::factory()) + ->create(); + + $this->json('GET', route('form.contribution', [ + 'type' => CitySolingenDocument::class, + 'form' => $form, + ]))->assertStatus(422); +}); + +it('throws error when form doesnt have participants', function () { + $this->login()->loginNami(); + + $form = Form::factory()->fields([])->create(); + + $this->json('GET', route('form.contribution', [ + 'type' => CitySolingenDocument::class, + 'form' => $form, + 'validate' => '1', + ]))->assertJsonValidationErrors(['participants' => 'Veranstaltung besitzt noch keine Teilnehmer*innen.']); +}); + +it('creates document when fields are present', function () { + Tex::spy(); + $this->login()->loginNami(); + + $form = Form::factory()->fields([ + test()->textField('fn')->specialType(SpecialType::FIRSTNAME), + test()->textField('ln')->specialType(SpecialType::LASTNAME), + test()->dateField('bd')->specialType(SpecialType::BIRTHDAY), + test()->dateField('zip')->specialType(SpecialType::ZIP), + test()->dateField('loc')->specialType(SpecialType::LOCATION), + test()->dateField('add')->specialType(SpecialType::ADDRESS), + ]) + ->has(Participant::factory()->data(['fn' => 'Baum', 'ln' => 'Muster', 'bd' => '1991-05-06', 'zip' => '33333', 'loc' => 'Musterstadt', 'add' => 'Laastr 4'])) + ->create(); + + $this->json('GET', route('form.contribution', [ + 'type' => CitySolingenDocument::class, + 'form' => $form, + ]))->assertOk(); + Tex::assertCompiled(CitySolingenDocument::class, fn ($document) => $document->hasAllContent(['Baum', 'Muster', '1991', 'Musterstadt', 'Laastr 4', '33333'])); +}); +