From 6bb8967c9efa6d61ea08fe2d8f7984bda548b281 Mon Sep 17 00:00:00 2001
From: philipp lang <philipp@aweos.de>
Date: Wed, 16 Nov 2022 22:59:49 +0100
Subject: [PATCH] Add memberships test

---
 .../Actions/MembershipDestroyAction.php       |  37 +++++
 .../Actions/MembershipStoreAction.php         |  84 ++++++++++
 tests/Feature/Member/StoreTest.php            |  81 ++++++++--
 tests/Feature/Member/UpdateTest.php           |  63 ++------
 tests/Feature/Membership/StoreTest.php        | 143 ++++++++++++++++--
 .../RequestFactories/MemberRequestFactory.php |  42 +++++
 .../MembershipRequestFactory.php              |  47 ++++++
 7 files changed, 429 insertions(+), 68 deletions(-)
 create mode 100644 app/Membership/Actions/MembershipDestroyAction.php
 create mode 100644 app/Membership/Actions/MembershipStoreAction.php
 create mode 100644 tests/RequestFactories/MemberRequestFactory.php
 create mode 100644 tests/RequestFactories/MembershipRequestFactory.php

diff --git a/app/Membership/Actions/MembershipDestroyAction.php b/app/Membership/Actions/MembershipDestroyAction.php
new file mode 100644
index 00000000..4fb30944
--- /dev/null
+++ b/app/Membership/Actions/MembershipDestroyAction.php
@@ -0,0 +1,37 @@
+<?php
+
+namespace App\Membership\Actions;
+
+use App\Member\Member;
+use App\Member\Membership;
+use App\Setting\NamiSettings;
+use Illuminate\Http\RedirectResponse;
+use Lorisleiva\Actions\ActionRequest;
+use Lorisleiva\Actions\Concerns\AsAction;
+
+class MembershipDestroyAction
+{
+    use AsAction;
+
+    public function handle(Member $member, Membership $membership, NamiSettings $settings): void
+    {
+        $api = $settings->login();
+        $settings->login()->deleteMembership(
+            $member->nami_id,
+            $api->membership($member->nami_id, $membership->nami_id)
+        );
+        $membership->delete();
+        $member->syncVersion();
+    }
+
+    public function asController(Member $member, Membership $membership, ActionRequest $request, NamiSettings $settings): RedirectResponse
+    {
+        $this->handle(
+            $member,
+            $membership,
+            $settings,
+        );
+
+        return redirect()->back();
+    }
+}
diff --git a/app/Membership/Actions/MembershipStoreAction.php b/app/Membership/Actions/MembershipStoreAction.php
new file mode 100644
index 00000000..f48b5273
--- /dev/null
+++ b/app/Membership/Actions/MembershipStoreAction.php
@@ -0,0 +1,84 @@
+<?php
+
+namespace App\Membership\Actions;
+
+use App\Activity;
+use App\Member\Member;
+use App\Member\Membership;
+use App\Setting\NamiSettings;
+use App\Subactivity;
+use Illuminate\Http\RedirectResponse;
+use Illuminate\Validation\Rule;
+use Illuminate\Validation\Rules\In;
+use Illuminate\Validation\ValidationException;
+use Lorisleiva\Actions\ActionRequest;
+use Lorisleiva\Actions\Concerns\AsAction;
+use Zoomyboy\LaravelNami\Data\Membership as NamiMembership;
+use Zoomyboy\LaravelNami\NamiException;
+
+class MembershipStoreAction
+{
+    use AsAction;
+
+    public function handle(Member $member, Activity $activity, ?Subactivity $subactivity, NamiSettings $settings): Membership
+    {
+        $from = now()->startOfDay();
+
+        try {
+            $namiId = $settings->login()->putMembership($member->nami_id, NamiMembership::fromArray([
+                'startsAt' => $from,
+                'groupId' => $member->group->nami_id,
+                'activityId' => $activity->nami_id,
+                'subactivityId' => $subactivity ? $subactivity->nami_id : null,
+            ]));
+        } catch (NamiException $e) {
+            throw ValidationException::withMessages(['nami' => htmlspecialchars($e->getMessage())]);
+        }
+
+        $membership = $member->memberships()->create([
+            'activity_id' => $activity->id,
+            'subactivity_id' => $subactivity ? $subactivity->id : null,
+            ...['nami_id' => $namiId, 'group_id' => $member->group->id, 'from' => $from],
+        ]);
+
+        $member->syncVersion();
+
+        return $membership;
+    }
+
+    /**
+     * @return array<string, array<int, string|In>>
+     */
+    public function rules(): array
+    {
+        $subactivityRule = request()->activity_id ? ['nullable', Rule::exists('activity_subactivity', 'subactivity_id')->where('activity_id', request()->activity_id)] : ['nullable'];
+
+        return [
+            'activity_id' => ['bail', 'required', 'exists:activities,id'],
+            'subactivity_id' => $subactivityRule,
+        ];
+    }
+
+    /**
+     * @return array<string, string>
+     */
+    public function getValidationAttributes(): array
+    {
+        return [
+            'activity_id' => 'Tätigkeit',
+            'subactivity_id' => 'Untertätigkeit',
+        ];
+    }
+
+    public function asController(Member $member, ActionRequest $request, NamiSettings $settings): RedirectResponse
+    {
+        $this->handle(
+            $member,
+            Activity::find($request->activity_id),
+            $request->subactivity_id ? Subactivity::find($request->subactivity_id) : null,
+            $settings,
+        );
+
+        return redirect()->back();
+    }
+}
diff --git a/tests/Feature/Member/StoreTest.php b/tests/Feature/Member/StoreTest.php
index 80984f69..68b6dc65 100644
--- a/tests/Feature/Member/StoreTest.php
+++ b/tests/Feature/Member/StoreTest.php
@@ -7,15 +7,13 @@ use App\Country;
 use App\Fee;
 use App\Gender;
 use App\Letter\BillKind;
-use App\Member\CreateJob;
+use App\Member\Actions\NamiPutMemberAction;
 use App\Member\Member;
 use App\Nationality;
 use App\Payment\Subscription;
 use App\Region;
-use App\Setting\NamiSettings;
 use App\Subactivity;
 use Illuminate\Foundation\Testing\DatabaseTransactions;
-use Illuminate\Support\Facades\Queue;
 use Tests\Lib\MergesAttributes;
 use Tests\TestCase;
 
@@ -26,12 +24,7 @@ class StoreTest extends TestCase
 
     public function testItCanStoreAMember(): void
     {
-        Queue::fake();
         Fee::factory()->create();
-        NamiSettings::fake([
-            'default_group_id' => 55,
-            'password' => 'tt',
-        ]);
         $this->withoutExceptionHandling()->login()->loginNami();
         $country = Country::factory()->create();
         $gender = Gender::factory()->create();
@@ -41,6 +34,7 @@ class StoreTest extends TestCase
         $subactivity = Subactivity::factory()->create();
         $subscription = Subscription::factory()->create();
         $billKind = BillKind::factory()->create();
+        NamiPutMemberAction::allowToRun();
 
         $response = $this
             ->from('/member/create')
@@ -58,7 +52,6 @@ class StoreTest extends TestCase
         $response->assertStatus(302)->assertSessionHasNoErrors();
         $response->assertRedirect('/member');
         $member = Member::firstWhere('firstname', 'Joe');
-        Queue::assertPushed(CreateJob::class, fn ($job) => $job->memberId === $member->id);
         $this->assertDatabaseHas('members', [
             'address' => 'Bavert 50',
             'bill_kind_id' => $billKind->id,
@@ -81,6 +74,76 @@ class StoreTest extends TestCase
             'zip' => '42719',
             'fax' => '+49 666',
         ]);
+        NamiPutMemberAction::spy()->shouldHaveReceived('handle')->withArgs(fn (Member $memberParam, Activity $activityParam, Subactivity $subactivityParam) => $memberParam->is($member)
+            && $activityParam->is($activity)
+            && $subactivityParam->is($subactivity)
+        )->once();
+    }
+
+    public function testItCanStoreAMemberWithoutNami(): void
+    {
+        Fee::factory()->create();
+        $this->withoutExceptionHandling()->login()->loginNami();
+        $country = Country::factory()->create();
+        $gender = Gender::factory()->create();
+        $region = Region::factory()->create();
+        $nationality = Nationality::factory()->create();
+        $subscription = Subscription::factory()->create();
+        $billKind = BillKind::factory()->create();
+        $activity = Activity::factory()->create();
+        $subactivity = Subactivity::factory()->create();
+        NamiPutMemberAction::allowToRun();
+
+        $response = $this
+            ->from('/member/create')
+            ->post('/member', $this->attributes([
+                'country_id' => $country->id,
+                'gender_id' => $gender->id,
+                'region_id' => $region->id,
+                'nationality_id' => $nationality->id,
+                'first_activity_id' => $activity->id,
+                'first_subactivity_id' => $subactivity->id,
+                'subscription_id' => $subscription->id,
+                'bill_kind_id' => $billKind->id,
+                'has_nami' => false,
+            ]));
+
+        $response->assertStatus(302)->assertSessionHasNoErrors();
+        $response->assertRedirect('/member');
+        $member = Member::firstWhere('firstname', 'Joe');
+        $this->assertDatabaseHas('members', [
+            'nami_id' => null,
+        ]);
+        NamiPutMemberAction::spy()->shouldNotHaveReceived('handle');
+    }
+
+    public function testSubscriptionIsRequiredIfFirstActivityIsPaid(): void
+    {
+        $this->login()->loginNami();
+        Fee::factory()->create();
+        $country = Country::factory()->create();
+        $gender = Gender::factory()->create();
+        $region = Region::factory()->create();
+        $nationality = Nationality::factory()->create();
+        $subscription = Subscription::factory()->create();
+        $billKind = BillKind::factory()->create();
+        $activity = Activity::factory()->create();
+        $subactivity = Subactivity::factory()->create();
+
+        $response = $this
+            ->from('/member/create')
+            ->post('/member', $this->attributes([
+                'country_id' => $country->id,
+                'gender_id' => $gender->id,
+                'region_id' => $region->id,
+                'nationality_id' => $nationality->id,
+                'first_activity_id' => $activity->id,
+                'first_subactivity_id' => $subactivity->id,
+                'subscription_id' => null,
+                'bill_kind_id' => $billKind->id,
+            ]));
+
+        $this->assertErrors(['subscription_id' => 'Beitragsart ist erforderlich.'], $response);
     }
 
     public function defaults(): array
diff --git a/tests/Feature/Member/UpdateTest.php b/tests/Feature/Member/UpdateTest.php
index 68d10f19..109eb2e7 100644
--- a/tests/Feature/Member/UpdateTest.php
+++ b/tests/Feature/Member/UpdateTest.php
@@ -4,8 +4,11 @@ namespace Tests\Feature\Member;
 
 use App\Confession;
 use App\Country;
+use App\Enum\Activity;
+use App\Enum\Subactivity;
 use App\Fee;
 use App\Group;
+use App\Member\Actions\NamiPutMemberAction;
 use App\Member\Member;
 use App\Nationality;
 use App\Payment\Subscription;
@@ -23,43 +26,17 @@ class UpdateTest extends TestCase
         $this->withoutExceptionHandling()->login()->loginNami();
         $member = $this->member();
         $this->fakeRequest();
+        NamiPutMemberAction::allowToRun();
 
         $response = $this
             ->from("/member/{$member->id}")
             ->patch("/member/{$member->id}", array_merge($member->getAttributes(), ['has_nami' => true]));
 
         $response->assertRedirect('/member');
-    }
-
-    public function testItHasPutRequest(): void
-    {
-        $this->withoutExceptionHandling()->login()->loginNami();
-        $member = $this->member();
-        $this->fakeRequest();
-
-        $response = $this
-            ->patch("/member/{$member->id}", array_merge($member->getAttributes(), ['has_nami' => true, 'firstname' => '::firstname::']));
-
-        Http::assertSent(fn ($request) => 'PUT' === $request->method()
-            && '::firstname::' === $request['vorname']
-        );
-    }
-
-    public function testItMergesExistingData(): void
-    {
-        $this->withoutExceptionHandling()->login()->loginNami();
-        $member = $this->member();
-        $this->fakeRequest();
-
-        $response = $this
-            ->from("/member/{$member->id}")
-            ->patch("/member/{$member->id}", array_merge($member->getAttributes(), ['has_nami' => true, 'firstname' => '::firstname::']));
-
-        Http::assertSent(fn ($request) => 'PUT' === $request->method()
-            && '{"a":"b"}' === $request['kontoverbindung']
-            && 'missingvalue' === $request['missingkey']
-            && '::firstname::' === $request['vorname']
-        );
+        NamiPutMemberAction::spy()->shouldHaveReceived('handle')->withArgs(fn (Member $memberParam, ?Activity $activityParam, ?Subactivity $subactivityParam) => $memberParam->is($member)
+            && null === $activityParam
+            && null === $subactivityParam
+        )->once();
     }
 
     public function testItChecksVersion(): void
@@ -76,23 +53,10 @@ class UpdateTest extends TestCase
         $response->assertRedirect("/member/{$member->id}/edit?conflict=1");
     }
 
-    public function testItUpdatesVersion(): void
-    {
-        $this->withoutExceptionHandling()->login()->loginNami();
-        $member = $this->member();
-        $this->fakeRequest();
-
-        $response = $this
-            ->from("/member/{$member->id}")
-            ->patch("/member/{$member->id}", array_merge($member->getAttributes(), ['has_nami' => true]));
-
-        $this->assertEquals(44, $member->fresh()->version);
-    }
-
     public function testItUpdatesCriminalRecord(): void
     {
         $this->withoutExceptionHandling()->login()->loginNami();
-        $member = $this->member();
+        $member = $this->member(['nami_id' => null]);
         $this->fakeRequest();
 
         $response = $this
@@ -105,7 +69,7 @@ class UpdateTest extends TestCase
                 'efz' => '2021-02-03',
                 'without_education_at' => '2021-02-04',
                 'without_efz_at' => '2021-02-05',
-                'has_nami' => true,
+                'has_nami' => false,
                 'multiply_pv' => true,
                 'multiply_more_pv' => true,
             ]));
@@ -121,7 +85,10 @@ class UpdateTest extends TestCase
         $this->assertEquals('2021-02-05', $member->fresh()->without_efz_at);
     }
 
-    private function member(): Member
+    /**
+     * @param array<string, string|Activity|null> $overwrites
+     */
+    private function member(array $overwrites = []): Member
     {
         return Member::factory()
             ->for(Group::factory()->state(['nami_id' => 10]))
@@ -129,7 +96,7 @@ class UpdateTest extends TestCase
             ->for(Nationality::factory())
             ->for(Subscription::factory()->for(Fee::factory()))
             ->for(Country::factory())
-            ->create(['nami_id' => 135]);
+            ->create(['nami_id' => 135, ...$overwrites]);
     }
 
     private function fakeRequest(): void
diff --git a/tests/Feature/Membership/StoreTest.php b/tests/Feature/Membership/StoreTest.php
index a380bd90..1b07ce9e 100644
--- a/tests/Feature/Membership/StoreTest.php
+++ b/tests/Feature/Membership/StoreTest.php
@@ -8,6 +8,7 @@ use App\Member\Member;
 use App\Subactivity;
 use Carbon\Carbon;
 use Illuminate\Foundation\Testing\DatabaseTransactions;
+use Tests\RequestFactories\MembershipRequestFactory;
 use Tests\TestCase;
 use Zoomyboy\LaravelNami\Fakes\MemberFake;
 use Zoomyboy\LaravelNami\Fakes\MembershipFake;
@@ -16,27 +17,35 @@ class StoreTest extends TestCase
 {
     use DatabaseTransactions;
 
-    public function testItCanCreateAMembership(): void
+    public function setUp(): void
     {
+        parent::setUp();
+
         Carbon::setTestNow(Carbon::parse('2022-02-03 03:00:00'));
-        $this->withoutExceptionHandling()->login()->loginNami();
+        $this->login()->loginNami();
+    }
+
+    public function testItCreatesAMembership(): void
+    {
+        $this->withoutExceptionHandling();
         app(MembershipFake::class)->createsSuccessfully(6, 133);
         app(MemberFake::class)->shows(1400, 6, ['version' => 1506]);
         $member = Member::factory()
             ->defaults()
             ->for(Group::factory()->inNami(1400))
             ->inNami(6)
-            ->createOne();
+            ->create();
         $activity = Activity::factory()
-            ->inNami(1)
-            ->hasAttached(Subactivity::factory()->inNami(2))
+            ->inNami(6)
+            ->hasAttached(Subactivity::factory()->inNami(4))
             ->createOne();
 
-        $this->post("/member/{$member->id}/membership", [
-            'activity_id' => $activity->id,
-            'subactivity_id' => $activity->subactivities->first()->id,
-        ]);
+        $response = $this->from('/member')->post(
+            "/member/{$member->id}/membership",
+            MembershipRequestFactory::new()->in($activity, $activity->subactivities->first())->create()
+        );
 
+        $response->assertRedirect('/member');
         $this->assertEquals(1506, $member->fresh()->version);
         $this->assertDatabaseHas('memberships', [
             'member_id' => $member->id,
@@ -45,11 +54,123 @@ class StoreTest extends TestCase
             'nami_id' => 133,
         ]);
         app(MembershipFake::class)->assertCreated(6, [
-            'untergliederungId' => 2,
-            'taetigkeitId' => 1,
+            'untergliederungId' => 4,
+            'taetigkeitId' => 6,
             'gruppierungId' => 1400,
             'aktivVon' => '2022-02-03T00:00:00',
             'aktivBis' => null,
         ]);
     }
+
+    public function testActivityIsRequired(): void
+    {
+        $member = Member::factory()
+            ->defaults()
+            ->for(Group::factory()->inNami(1400))
+            ->inNami(6)
+            ->create();
+
+        $response = $this->post(
+            "/member/{$member->id}/membership",
+            MembershipRequestFactory::new()->missingAll()->create(),
+        );
+
+        $this->assertErrors(['activity_id' => 'Tätigkeit ist erforderlich.'], $response);
+    }
+
+    public function testActivityShouldBeValid(): void
+    {
+        $member = Member::factory()
+            ->defaults()
+            ->for(Group::factory()->inNami(1400))
+            ->inNami(6)
+            ->create();
+
+        $response = $this->post(
+            "/member/{$member->id}/membership",
+            MembershipRequestFactory::new()->invalidActivity()->create(),
+        );
+
+        $this->assertErrors(['activity_id' => 'Tätigkeit ist nicht vorhanden.'], $response);
+    }
+
+    public function testSubactivityShouldBeFromActivity(): void
+    {
+        $member = Member::factory()
+            ->defaults()
+            ->for(Group::factory()->inNami(1400))
+            ->inNami(6)
+            ->create();
+
+        $response = $this->post(
+            "/member/{$member->id}/membership",
+            MembershipRequestFactory::new()->unmatchingSubactivity()->create(),
+        );
+
+        $this->assertErrors(['subactivity_id' => 'Untertätigkeit ist nicht vorhanden.'], $response);
+    }
+
+    public function testSubactivityCanBeEmpty(): void
+    {
+        $this->withoutExceptionHandling();
+        app(MembershipFake::class)->createsSuccessfully(6, 133);
+        app(MemberFake::class)->shows(1400, 6, ['version' => 1506]);
+        $member = Member::factory()
+            ->defaults()
+            ->for(Group::factory()->inNami(1400))
+            ->inNami(6)
+            ->create();
+        $activity = Activity::factory()
+            ->inNami(6)
+            ->createOne();
+
+        $this->post(
+            "/member/{$member->id}/membership",
+            MembershipRequestFactory::new()->in($activity, null)->create()
+        );
+
+        $this->assertEquals(1506, $member->fresh()->version);
+        $this->assertDatabaseHas('memberships', [
+            'member_id' => $member->id,
+            'activity_id' => $activity->id,
+            'subactivity_id' => null,
+            'nami_id' => 133,
+        ]);
+        app(MembershipFake::class)->assertCreated(6, [
+            'untergliederungId' => null,
+            'taetigkeitId' => 6,
+            'gruppierungId' => 1400,
+            'aktivVon' => '2022-02-03T00:00:00',
+            'aktivBis' => null,
+        ]);
+    }
+
+    /**
+     * @testWith ["namierror<br>", "namierror&lt;br&gt;"]
+     *           ["", "Erstellen der Mitgliedschaft fehlgeschlagen"]
+     */
+    public function testItReturnsNamiError(string $namiError, string $validationError): void
+    {
+        app(MembershipFake::class)->failsCreating(6, $namiError);
+        $member = Member::factory()
+            ->defaults()
+            ->for(Group::factory()->inNami(1400))
+            ->inNami(6)
+            ->create();
+        $activity = Activity::factory()
+            ->inNami(6)
+            ->hasAttached(Subactivity::factory()->inNami(4))
+            ->createOne();
+
+        $response = $this->post(
+            "/member/{$member->id}/membership",
+            MembershipRequestFactory::new()->in($activity, $activity->subactivities->first())->create()
+        );
+
+        $this->assertErrors(['nami' => $validationError], $response);
+
+        $this->assertDatabaseMissing('memberships', [
+            'member_id' => $member->id,
+        ]);
+    }
 }
diff --git a/tests/RequestFactories/MemberRequestFactory.php b/tests/RequestFactories/MemberRequestFactory.php
new file mode 100644
index 00000000..9adb65f2
--- /dev/null
+++ b/tests/RequestFactories/MemberRequestFactory.php
@@ -0,0 +1,42 @@
+<?php
+
+namespace Tests\RequestFactories;
+
+use Worksome\RequestFactories\RequestFactory;
+
+class MemberRequestFactory extends RequestFactory
+{
+    public function definition(): array
+    {
+        return [
+            'address' => 'Bavert 50',
+            'birthday' => '2013-02-19',
+            'children_phone' => '+49 123 44444',
+            'efz' => '',
+            'email' => '',
+            'email_parents' => 'osloot@aol.com',
+            'fax' => '+49 666',
+            'firstname' => 'Joe',
+            'further_address' => '',
+            'has_nami' => true,
+            'has_svk' => false,
+            'has_vk' => false,
+            'joined_at' => '2022-08-12',
+            'lastname' => 'Muster',
+            'letter_address' => '',
+            'location' => 'Solingen',
+            'main_phone' => '+49 212 2334322',
+            'mobile_phone' => '+49 157 53180451',
+            'more_ps_at' => '',
+            'multiply_more_pv' => false,
+            'multiply_pv' => false,
+            'other_country' => '',
+            'ps_at' => '',
+            'send_newspaper' => true,
+            'without_education_at' => '',
+            'without_efz_at' => '',
+            'work_phone' => '',
+            'zip' => '42719',
+        ];
+    }
+}
diff --git a/tests/RequestFactories/MembershipRequestFactory.php b/tests/RequestFactories/MembershipRequestFactory.php
new file mode 100644
index 00000000..6a8051cb
--- /dev/null
+++ b/tests/RequestFactories/MembershipRequestFactory.php
@@ -0,0 +1,47 @@
+<?php
+
+namespace Tests\RequestFactories;
+
+use App\Activity;
+use App\Subactivity;
+use Worksome\RequestFactories\RequestFactory;
+
+class MembershipRequestFactory extends RequestFactory
+{
+    public function definition(): array
+    {
+        return [];
+    }
+
+    public function in(Activity $activity, ?Subactivity $subactivity = null): self
+    {
+        return $this->state([
+            'activity_id' => $activity->id,
+            'subactivity_id' => $subactivity ? $subactivity->id : null,
+        ]);
+    }
+
+    public function missingAll(): self
+    {
+        return $this->state([
+            'activity_id' => null,
+            'subactivity_id' => null,
+        ]);
+    }
+
+    public function invalidActivity(): self
+    {
+        return $this->state([
+            'activity_id' => 10000,
+            'subactivity_id' => null,
+        ]);
+    }
+
+    public function unmatchingSubactivity(): self
+    {
+        return $this->state([
+            'activity_id' => Activity::factory()->create()->id,
+            'subactivity_id' => Subactivity::factory()->create()->id,
+        ]);
+    }
+}