diff --git a/app/Member/Actions/NamiPutMemberAction.php b/app/Member/Actions/NamiPutMemberAction.php new file mode 100644 index 00000000..5d2c141d --- /dev/null +++ b/app/Member/Actions/NamiPutMemberAction.php @@ -0,0 +1,55 @@ +login(); + $response = $api->putMember([ + 'firstname' => $member->firstname, + 'lastname' => $member->lastname, + 'joined_at' => $member->joined_at, + 'birthday' => $member->birthday, + 'send_newspaper' => $member->send_newspaper, + 'address' => $member->address, + 'zip' => $member->zip, + 'location' => $member->location, + 'nickname' => $member->nickname, + 'other_country' => $member->other_country, + 'further_address' => $member->further_address, + 'main_phone' => $member->main_phone, + 'mobile_phone' => $member->mobile_phone, + 'work_phone' => $member->work_phone, + 'fax' => $member->fax, + 'email' => $member->email, + 'email_parents' => $member->email_parents, + 'gender_id' => optional($member->gender)->nami_id, + 'confession_id' => $member->confession ? $member->confession->nami_id : Confession::firstWhere('is_null', true)->nami_id, + 'region_id' => optional($member->region)->nami_id, + 'country_id' => $member->country->nami_id, + 'fee_id' => $member->getNamiFeeId(), + 'nationality_id' => $member->nationality->nami_id, + 'group_id' => $member->group->nami_id, + 'first_activity_id' => $activity ? $activity->nami_id : null, + 'first_subactivity_id' => $subactivity ? $subactivity->nami_id : null, + 'id' => $member->nami_id, + 'version' => $member->version, + ]); + Member::withoutEvents(function () use ($response, $member, $api) { + $member->update(['nami_id' => $response['id']]); + app(MemberPullAction::class)->api($api)->member($member->group->nami_id, $member->nami_id)->execute(); + }); + } +} diff --git a/app/Member/MemberRequest.php b/app/Member/MemberRequest.php index ee1a834b..f3dca5be 100644 --- a/app/Member/MemberRequest.php +++ b/app/Member/MemberRequest.php @@ -4,7 +4,9 @@ namespace App\Member; use App\Activity; use App\Group; +use App\Member\Actions\NamiPutMemberAction; use App\Setting\NamiSettings; +use App\Subactivity; use Illuminate\Foundation\Http\FormRequest; use Illuminate\Support\Str; use Illuminate\Validation\Rule; @@ -29,8 +31,10 @@ class MemberRequest extends FormRequest public function rules() { return [ - 'first_activity_id' => Rule::requiredIf(fn () => 'POST' == $this->method()), - 'first_subactivity_id' => Rule::requiredIf(fn () => 'POST' == $this->method()), + ...'POST' === $this->method() ? [ + 'first_activity' => 'exclude|required', + 'first_subactivity' => 'exclude|required', + ] : [], 'subscription_id' => Rule::requiredIf(function () { if ('POST' != $this->method()) { return false; @@ -84,7 +88,11 @@ class MemberRequest extends FormRequest 'group_id' => Group::where('nami_id', $settings->default_group_id)->firstOrFail()->id, ]); if ($this->input('has_nami')) { - CreateJob::dispatch($member); + NamiPutMemberAction::run( + $member, + Activity::findOrFail($this->input('first_activity_id')), + Subactivity::find($this->input('first_subactivity_id')), + ); } } @@ -97,10 +105,10 @@ class MemberRequest extends FormRequest $member->save(); if ($this->input('has_nami') && null === $member->nami_id) { - CreateJob::dispatch($member); + NamiPutMemberAction::run($member->fresh(), null, null); } if ($this->input('has_nami') && null !== $member->nami_id && $namiSync) { - UpdateJob::dispatch($member->fresh()); + NamiPutMemberAction::run($member->fresh(), null, null); } if (!$this->input('has_nami') && null !== $member->nami_id) { DeleteJob::dispatch($member->nami_id); diff --git a/app/Member/Membership.php b/app/Member/Membership.php index ef7ad9ef..2010b938 100644 --- a/app/Member/Membership.php +++ b/app/Member/Membership.php @@ -4,10 +4,14 @@ namespace App\Member; use App\Activity; use App\Subactivity; +use Illuminate\Database\Eloquent\Builder; use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Relations\BelongsTo; +/** + * @property int $count + */ class Membership extends Model { use HasFactory; @@ -23,4 +27,44 @@ class Membership extends Model { return $this->belongsTo(Subactivity::class); } + + /** + * @param Builder $query + * + * @return Builder + */ + public function scopeIsAgeGroup(Builder $query): Builder + { + return $query->whereHas('subactivity', fn ($builder) => $builder->where('is_age_group', true)); + } + + /** + * @param Builder $query + * + * @return Builder + */ + public function scopeIsMember(Builder $query): Builder + { + return $query->whereHas('activity', fn ($builder) => $builder->where('is_member', true)); + } + + /** + * @param Builder $query + * + * @return Builder + */ + public function scopeIsLeader(Builder $query): Builder + { + return $query->whereHas('activity', fn ($builder) => $builder->where('has_efz', true)); + } + + /** + * @param Builder $query + * + * @return Builder + */ + public function scopeTrying(Builder $query): Builder + { + return $query->whereHas('activity', fn ($builder) => $builder->where('is_try', true)); + } } diff --git a/app/Member/UpdateJob.php b/app/Member/UpdateJob.php deleted file mode 100644 index 58fc310f..00000000 --- a/app/Member/UpdateJob.php +++ /dev/null @@ -1,73 +0,0 @@ -memberId = $member->id; - } - - /** - * Execute the job. - * - * @return void - */ - public function handle(NamiSettings $settings) - { - $this->member = Member::find($this->memberId); - - if (!$this->member->hasNami) { - return; - } - - $response = $settings->login()->putMember([ - 'firstname' => $this->member->firstname, - 'lastname' => $this->member->lastname, - 'joined_at' => $this->member->joined_at, - 'birthday' => $this->member->birthday, - 'send_newspaper' => $this->member->send_newspaper, - 'address' => $this->member->address, - 'zip' => $this->member->zip, - 'location' => $this->member->location, - 'nickname' => $this->member->nickname, - 'other_country' => $this->member->other_country, - 'further_address' => $this->member->further_address, - 'main_phone' => $this->member->main_phone, - 'mobile_phone' => $this->member->mobile_phone, - 'work_phone' => $this->member->work_phone, - 'fax' => $this->member->fax, - 'email' => $this->member->email, - 'email_parents' => $this->member->email_parents, - 'gender_id' => optional($this->member->gender)->nami_id, - 'confession_id' => $this->member->confession ? $this->member->confession->nami_id : Confession::firstWhere('is_null', true)->id, - 'region_id' => optional($this->member->region)->nami_id, - 'country_id' => $this->member->country->nami_id, - 'fee_id' => $this->member->getNamiFeeId(), - 'nationality_id' => $this->member->nationality->nami_id, - 'id' => $this->member->nami_id, - 'group_id' => $this->member->group->nami_id, - 'version' => $this->member->version, - ]); - Member::withoutEvents(function () use ($response) { - $this->member->update(['version' => $response['version']]); - }); - } -} diff --git a/app/Membership/MembershipController.php b/app/Membership/MembershipController.php index 6d5382e7..840bb367 100644 --- a/app/Membership/MembershipController.php +++ b/app/Membership/MembershipController.php @@ -5,19 +5,11 @@ namespace App\Membership; use App\Http\Controllers\Controller; use App\Member\Member; use App\Member\Membership; -use App\Membership\Requests\StoreRequest; use App\Setting\NamiSettings; use Illuminate\Http\RedirectResponse; class MembershipController extends Controller { - public function store(Member $member, StoreRequest $request, NamiSettings $settings): RedirectResponse - { - $request->persist($member, $settings); - - return redirect()->back(); - } - public function destroy(Member $member, Membership $membership, NamiSettings $settings): RedirectResponse { $api = $settings->login(); diff --git a/app/Membership/Requests/StoreRequest.php b/app/Membership/Requests/StoreRequest.php deleted file mode 100644 index 5d648a7b..00000000 --- a/app/Membership/Requests/StoreRequest.php +++ /dev/null @@ -1,52 +0,0 @@ -startOfDay(); - $namiId = $settings->login()->putMembership($member->nami_id, Membership::fromArray([ - 'startsAt' => $from, - 'groupId' => $member->group->nami_id, - 'activityId' => Activity::find($this->input('activity_id'))->nami_id, - 'subactivityId' => optional(Subactivity::find($this->input('subactivity_id')))->nami_id, - ])); - - $member->memberships()->create([ - ...$this->input(), - ...['nami_id' => $namiId, 'group_id' => $member->group->id, 'from' => $from], - ]); - - $member->syncVersion(); - } -} diff --git a/database/factories/ActivityFactory.php b/database/factories/ActivityFactory.php index 01b8b1d3..bb685935 100644 --- a/database/factories/ActivityFactory.php +++ b/database/factories/ActivityFactory.php @@ -12,6 +12,31 @@ class ActivityFactory extends Factory { protected $model = Activity::class; + /** @var array */ + private array $tries = [ + 'Schnuppermitgliedschaft', + ]; + + /** @var array */ + private array $members = [ + '€ Mitglied', + 'Schnuppermitgliedschaft', + ]; + + /** @var array */ + private array $filterableActivities = [ + '€ Mitglied', + '€ passive Mitgliedschaft', + '€ KassiererIn', + '€ LeiterIn', + 'Schnuppermitgliedschaft', + ]; + + /** @var array */ + private array $efz = [ + '€ LeiterIn', + ]; + /** * Define the model's default state. * @@ -32,6 +57,12 @@ class ActivityFactory extends Factory public function name(string $name): self { - return $this->state(['name' => $name]); + return $this->state([ + 'name' => $name, + 'is_try' => in_array($name, $this->tries), + 'is_member' => in_array($name, $this->members), + 'is_filterable' => in_array($name, $this->filterableActivities), + 'has_efz' => in_array($name, $this->efz), + ]); } } diff --git a/database/factories/ConfessionFactory.php b/database/factories/ConfessionFactory.php index ca64714a..18bfc3a2 100644 --- a/database/factories/ConfessionFactory.php +++ b/database/factories/ConfessionFactory.php @@ -21,4 +21,9 @@ class ConfessionFactory extends Factory 'is_null' => false, ]; } + + public function inNami(int $namiId): self + { + return $this->state(['nami_id' => $namiId]); + } } diff --git a/database/factories/Member/MembershipFactory.php b/database/factories/Member/MembershipFactory.php index 398a393d..e2c213eb 100644 --- a/database/factories/Member/MembershipFactory.php +++ b/database/factories/Member/MembershipFactory.php @@ -2,8 +2,10 @@ namespace Database\Factories\Member; +use App\Activity; use App\Group; use App\Member\Membership; +use App\Subactivity; use Illuminate\Database\Eloquent\Factories\Factory; /** @@ -30,4 +32,15 @@ class MembershipFactory extends Factory { return $this->state(['nami_id' => $namiId]); } + + public function in(string $activity, int $activityNamiId, ?string $subactivity = null, ?int $subactivityNamiId = null): self + { + $instance = $this->for(Activity::factory()->name($activity)->inNami($activityNamiId)); + + if ($subactivity) { + $instance = $instance->for(Subactivity::factory()->name($subactivity)->inNami($subactivityNamiId)); + } + + return $instance; + } } diff --git a/database/factories/NationalityFactory.php b/database/factories/NationalityFactory.php index a38f1b94..5ec3e64c 100644 --- a/database/factories/NationalityFactory.php +++ b/database/factories/NationalityFactory.php @@ -24,4 +24,9 @@ class NationalityFactory extends Factory 'nami_id' => $this->faker->randomNumber(), ]; } + + public function inNami(int $namiId): self + { + return $this->state(['nami_id' => $namiId]); + } } diff --git a/database/factories/SubactivityFactory.php b/database/factories/SubactivityFactory.php index d53b75e2..cb4fdfe7 100644 --- a/database/factories/SubactivityFactory.php +++ b/database/factories/SubactivityFactory.php @@ -12,6 +12,25 @@ class SubactivityFactory extends Factory { protected $model = Subactivity::class; + /** @var array */ + private array $filterableSubactivities = [ + 'Biber', + 'Wölfling', + 'Jungpfadfinder', + 'Pfadfinder', + 'Vorstand', + 'Rover', + ]; + + /** @var array */ + private array $ageGroups = [ + 'Biber', + 'Wölfling', + 'Jungpfadfinder', + 'Pfadfinder', + 'Rover', + ]; + /** * Define the model's default state. * @@ -35,13 +54,17 @@ class SubactivityFactory extends Factory return $this->state(['is_age_group' => true]); } - public function name(string $name): self - { - return $this->state(['name' => $name]); - } - public function filterable(): self { return $this->state(['is_filterable' => true]); } + + public function name(string $name): self + { + return $this->state([ + 'name' => $name, + 'is_filterable' => in_array($name, $this->filterableSubactivities), + 'is_age_group' => in_array($name, $this->ageGroups), + ]); + } } diff --git a/database/migrations/2022_01_18_205354_create_memberships_table.php b/database/migrations/2022_01_18_205354_create_memberships_table.php index f9d3f899..15f94ab1 100644 --- a/database/migrations/2022_01_18_205354_create_memberships_table.php +++ b/database/migrations/2022_01_18_205354_create_memberships_table.php @@ -16,7 +16,7 @@ class CreateMembershipsTable extends Migration Schema::create('memberships', function (Blueprint $table) { $table->id(); $table->foreignId('group_id')->constrained(); - $table->foreignId('member_id')->constrained(); + $table->unsignedBigInteger('member_id'); $table->unsignedInteger('nami_id')->nullable(); $table->datetime('from'); $table->timestamps(); diff --git a/resources/js/views/member/MemberMemberships.vue b/resources/js/views/member/MemberMemberships.vue index 3a79f4bb..c246b332 100644 --- a/resources/js/views/member/MemberMemberships.vue +++ b/resources/js/views/member/MemberMemberships.vue @@ -98,7 +98,7 @@ export default { var _self = this; var options = { - onFinish() { + onSuccess() { _self.single = null; _self.mode = null; }, diff --git a/routes/web.php b/routes/web.php index 16416424..7889b24a 100644 --- a/routes/web.php +++ b/routes/web.php @@ -9,7 +9,8 @@ use App\Initialize\Actions\InitializeFormAction; use App\Member\Controllers\MemberResyncController; use App\Member\MemberConfirmController; use App\Member\MemberController; -use App\Membership\MembershipController; +use App\Membership\Actions\MembershipDestroyAction; +use App\Membership\Actions\MembershipStoreAction; use App\Payment\AllpaymentController; use App\Payment\PaymentController; use App\Payment\SendpaymentController; @@ -33,7 +34,8 @@ Route::group(['middleware' => 'auth:web'], function (): void { ->name('member.singlepdf'); Route::get('/sendpayment', [SendpaymentController::class, 'create'])->name('sendpayment.create'); Route::get('/sendpayment/pdf', [SendpaymentController::class, 'send'])->name('sendpayment.pdf'); - Route::apiResource('member.membership', MembershipController::class); + Route::post('/member/{member}/membership', MembershipStoreAction::class)->name('membership.store'); + Route::delete('/member/{member}/membership/{membership}', MembershipDestroyAction::class)->name('membership.destroy'); Route::resource('member.course', CourseController::class); Route::get('/member/{member}/efz', ShowEfzDocumentAction::class)->name('efz'); Route::get('/member/{member}/resync', MemberResyncController::class)->name('member.resync'); diff --git a/tests/Feature/Member/DeleteTest.php b/tests/Feature/Member/DeleteTest.php index 0cbce3cb..3798d112 100644 --- a/tests/Feature/Member/DeleteTest.php +++ b/tests/Feature/Member/DeleteTest.php @@ -24,7 +24,7 @@ class DeleteTest extends TestCase $response->assertRedirect('/member'); - Queue::assertPushed(DeleteJob::class, fn ($job) => $job->memberId === $member->id); + Queue::assertPushed(DeleteJob::class, fn ($job) => 123 === $job->namiId); $this->assertDatabaseMissing('members', [ 'id' => $member->id, ]); @@ -52,9 +52,8 @@ class DeleteTest extends TestCase $this->withoutExceptionHandling()->login()->loginNami(); $member = Member::factory()->defaults()->inNami(123)->create(); - dispatch(new DeleteJob($member)); + dispatch(new DeleteJob(123)); app(MemberFake::class)->assertDeleted(123, Carbon::parse('yesterday')); - $this->assertNull($member->fresh()->nami_id); } } diff --git a/tests/Feature/Member/EditTest.php b/tests/Feature/Member/EditTest.php new file mode 100644 index 00000000..a0e302e5 --- /dev/null +++ b/tests/Feature/Member/EditTest.php @@ -0,0 +1,31 @@ +withoutExceptionHandling(); + $this->login()->loginNami(); + $member = Member::factory()->defaults()->create(['firstname' => 'Max']); + $activity = Activity::factory()->hasAttached(Subactivity::factory()->name('Biber'))->name('€ Mitglied')->create(); + $subactivity = $activity->subactivities->first(); + + $response = $this->get(route('member.edit', ['member' => $member])); + + $this->assertInertiaHas('Biber', $response, "subactivities.{$activity->id}.{$subactivity->id}"); + $this->assertInertiaHas('€ Mitglied', $response, "activities.{$activity->id}"); + $this->assertInertiaHas('Max', $response, 'data.firstname'); + $this->assertInertiaHas('edit', $response, 'mode'); + $this->assertInertiaHas(false, $response, 'conflict'); + } +} diff --git a/tests/Feature/Member/NamiPutMemberActionTest.php b/tests/Feature/Member/NamiPutMemberActionTest.php new file mode 100644 index 00000000..ab2b7e64 --- /dev/null +++ b/tests/Feature/Member/NamiPutMemberActionTest.php @@ -0,0 +1,90 @@ +create(); + $this->withoutExceptionHandling()->login()->loginNami(); + $country = Country::factory()->create(); + $gender = Gender::factory()->create(); + $region = Region::factory()->create(); + $nationality = Nationality::factory()->inNami(565)->create(); + $subscription = Subscription::factory()->create(); + $billKind = BillKind::factory()->create(); + $group = Group::factory()->inNami(55)->create(); + $confession = Confession::factory()->inNami(567)->create(['is_null' => true]); + app(MemberFake::class)->createsSuccessfully(55, 993); + $this->stubIo(MemberPullAction::class, fn ($mock) => $mock); + $activity = Activity::factory()->hasAttached(Subactivity::factory()->name('Biber')->inNami(55))->name('Leiter')->inNami(6)->create(); + $subactivity = $activity->subactivities->first(); + + $member = Member::factory() + ->for($country) + ->for($subscription) + ->for($region) + ->for($nationality) + ->for($billKind) + ->for($gender) + ->for($group) + ->create(); + + NamiPutMemberAction::run($member, $activity, $subactivity); + + app(MemberFake::class)->assertCreated(55, [ + 'ersteTaetigkeitId' => 6, + 'ersteUntergliederungId' => 55, + 'konfessionId' => 567, + ]); + $this->assertDatabaseHas('members', [ + 'nami_id' => 993, + ]); + } + + public function testItMergesExistingData(): void + { + $this->withoutExceptionHandling()->login()->loginNami(); + $group = Group::factory()->inNami(55)->create(); + $confession = Confession::factory()->inNami(567)->create(['is_null' => true]); + $member = Member::factory() + ->defaults() + ->inNami(556) + ->create(); + $this->stubIo(MemberPullAction::class, fn ($mock) => $mock); + + app(MemberFake::class)->shows(55, 556, [ + 'missingkey' => 'missingvalue', + 'kontoverbindung' => ['a' => 'b'], + ])->updates(55, 556, ['id' => 556]); + + NamiPutMemberAction::run($member, null, null); + + app(MemberFake::class)->assertUpdated(55, 556, [ + 'kontoverbindung' => '{"a":"b"}', + 'missingkey' => 'missingvalue', + ]); + $this->assertDatabaseHas('members', ['nami_id' => 556]); + } +} diff --git a/tests/Feature/Membership/DestroyTest.php b/tests/Feature/Membership/DestroyTest.php new file mode 100644 index 00000000..ba02a9dd --- /dev/null +++ b/tests/Feature/Membership/DestroyTest.php @@ -0,0 +1,57 @@ +login()->loginNami(); + } + + public function testItDestroysAMembership(): void + { + $this->withoutExceptionHandling(); + app(MembershipFake::class) + ->destroysSuccessfully(6, 1300) + ->shows(6, [ + 'id' => 1300, + 'gruppierungId' => 1400, + 'taetigkeitId' => 1, + 'untergliederungId' => 6, + 'aktivVon' => '2017-02-11 00:00:00', + 'aktivBis' => null, + ]); + app(MemberFake::class)->shows(1400, 6, ['version' => 1506]); + $member = Member::factory() + ->defaults() + ->for(Group::factory()->inNami(1400)) + ->has(Membership::factory()->inNami(1300)->in('€ Mitglied', 1, 'Rover', 6)) + ->inNami(6) + ->create(); + + $response = $this->from('/member')->delete("/member/{$member->id}/membership/{$member->memberships->first()->id}"); + + $response->assertRedirect('/member'); + $this->assertEquals(1506, $member->fresh()->version); + $this->assertDatabaseMissing('memberships', [ + 'member_id' => $member->id, + 'nami_id' => 1300, + ]); + app(MembershipFake::class)->assertDeleted(6, 1300); + } +}