diff --git a/app/Invoice/Actions/InvoiceStoreAction.php b/app/Invoice/Actions/InvoiceStoreAction.php index c1633c7e..deb4a7ea 100644 --- a/app/Invoice/Actions/InvoiceStoreAction.php +++ b/app/Invoice/Actions/InvoiceStoreAction.php @@ -25,8 +25,6 @@ class InvoiceStoreAction 'to.zip' => 'required|string|max:255', 'to.name' => 'required|string|max:255', 'greeting' => 'required|string|max:255', - 'intro' => 'required|string', - 'outro' => 'required|string', 'positions' => 'array', 'positions.*.description' => 'required|string|max:300', 'positions.*.price' => 'required|integer|min:0', diff --git a/app/Invoice/Actions/MassStoreAction.php b/app/Invoice/Actions/MassStoreAction.php new file mode 100644 index 00000000..a9995a1a --- /dev/null +++ b/app/Invoice/Actions/MassStoreAction.php @@ -0,0 +1,50 @@ + + */ + public function rules(): array + { + return [ + 'year' => 'required|numeric', + ]; + } + + public function handle(int $year): void + { + $memberGroup = Member::payable()->get() + ->groupBy(fn ($member) => "{$member->bill_kind->value}{$member->lastname}{$member->address}{$member->zip}{$member->location}"); + foreach ($memberGroup as $members) { + $invoice = Invoice::createForMember($members->first()); + + foreach ($members as $member) { + foreach ($member->subscription->children as $child) { + $invoice->positions()->create([ + 'description' => str($child->name)->replace('{name}', $member->firstname . ' ' . $member->lastname)->replace('{year}', $year), + 'price' => $child->amount, + 'member_id' => $member->id, + ]); + } + } + } + } + + public function asController(ActionRequest $request): JsonResponse + { + $this->handle($request->year); + + return response()->json([]); + } +} diff --git a/app/Invoice/Models/Invoice.php b/app/Invoice/Models/Invoice.php index 7eefd6cd..2d2c42d9 100644 --- a/app/Invoice/Models/Invoice.php +++ b/app/Invoice/Models/Invoice.php @@ -2,6 +2,8 @@ namespace App\Invoice\Models; +use App\Invoice\Enums\InvoiceStatus; +use App\Member\Member; use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Relations\HasMany; @@ -23,4 +25,18 @@ class Invoice extends Model { return $this->hasMany(InvoicePosition::class); } + + public static function createForMember(Member $member): self + { + return static::create([ + 'to' => [ + 'name' => 'Familie ' . $member->lastname, + 'address' => $member->address, + 'zip' => $member->zip, + 'location' => $member->location, + ], + 'greeting' => 'Liebe Familie ' . $member->lastname, + 'status' => InvoiceStatus::NEW, + ]); + } } diff --git a/app/Member/Member.php b/app/Member/Member.php index d1a56d6e..f0f109b8 100644 --- a/app/Member/Member.php +++ b/app/Member/Member.php @@ -357,18 +357,6 @@ class Member extends Model implements Geolocatable return $query->where('bill_kind', '!=', null)->where('subscription_id', '!=', null); } - /** - * @param Builder $query - * - * @return Builder - */ - public function scopeWhereNoPayment(Builder $query, int $year): Builder - { - return $query->whereDoesntHave('payments', function (Builder $q) use ($year) { - $q->where('nr', '=', $year); - }); - } - /** * @param Builder $query * diff --git a/app/Payment/Actions/AllpaymentStoreAction.php b/app/Payment/Actions/AllpaymentStoreAction.php deleted file mode 100644 index 2509d65c..00000000 --- a/app/Payment/Actions/AllpaymentStoreAction.php +++ /dev/null @@ -1,83 +0,0 @@ - - */ - public function rules(): array - { - return [ - 'year' => 'required|numeric', - 'for_promise' => 'present|boolean', - ]; - } - - public function handle(int $year, bool $forPromise): void - { - foreach (Member::payable()->whereNoPayment($year)->get() as $member) { - $member->createPayment([ - 'nr' => $year, - 'subscription_id' => $member->subscription_id, - 'status_id' => Status::default(), - ]); - - if (!$forPromise) { - continue; - } - - $this->createPaymentsForPromise($member, $year); - } - } - - private function createPaymentsForPromise(Member $member, int $year): void - { - $subscription = Subscription::firstWhere('for_promise', true); - - if (is_null($subscription)) { - return; - } - - foreach ($this->promisedMemberships($member, $year) as $membership) { - $attributes = [ - 'nr' => $membership->subactivity->name.' '.$membership->promised_at->year, - 'subscription_id' => $subscription->id, - ]; - - if (!$member->payments()->where($attributes)->exists()) { - $member->createPayment([ - ...$attributes, - 'status_id' => Status::default(), - ]); - } - } - } - - /** - * @return Collection - */ - public function promisedMemberships(Member $member, int $year): Collection - { - return $member->memberships()->whereNotNull('promised_at')->whereYear('promised_at', now()->year($year)->subYear())->get(); - } - - public function asController(ActionRequest $request): RedirectResponse - { - $this->handle($request->year, $request->for_promise); - - return redirect()->back()->success('Zahlungen erstellt'); - } -} diff --git a/database/factories/Member/MemberFactory.php b/database/factories/Member/MemberFactory.php index 171986c1..185b9ba5 100644 --- a/database/factories/Member/MemberFactory.php +++ b/database/factories/Member/MemberFactory.php @@ -95,4 +95,15 @@ class MemberFactory extends Factory } }); } + + public function sameFamilyAs(Member $member): self + { + return $this->state([ + 'firstname' => $member->firstname . 'a', + 'lastname' => $member->lastname, + 'address' => $member->address, + 'zip' => $member->zip, + 'location' => $member->location, + ]); + } } diff --git a/database/migrations/2023_12_12_015320_create_invoices_table.php b/database/migrations/2023_12_12_015320_create_invoices_table.php index e12b68b8..2b4e3f6b 100644 --- a/database/migrations/2023_12_12_015320_create_invoices_table.php +++ b/database/migrations/2023_12_12_015320_create_invoices_table.php @@ -17,8 +17,6 @@ return new class extends Migration $table->id(); $table->json('to'); $table->string('greeting'); - $table->text('intro'); - $table->text('outro'); $table->string('status'); $table->timestamps(); }); diff --git a/routes/web.php b/routes/web.php index 2588343f..9e3790b3 100644 --- a/routes/web.php +++ b/routes/web.php @@ -25,6 +25,7 @@ use App\Initialize\Actions\InitializeFormAction; use App\Initialize\Actions\NamiGetSearchLayerAction; use App\Initialize\Actions\NamiLoginCheckAction; use App\Initialize\Actions\NamiSearchAction; +use App\Invoice\Actions\MassStoreAction; use App\Maildispatcher\Actions\CreateAction; use App\Maildispatcher\Actions\DestroyAction; use App\Maildispatcher\Actions\EditAction; @@ -69,8 +70,6 @@ Route::group(['middleware' => 'auth:web'], function (): void { Route::resource('member', MemberController::class)->except('show', 'destroy'); Route::delete('/member/{member}', MemberDeleteAction::class); Route::get('/member/{member}', MemberShowAction::class)->name('member.show'); - Route::get('allpayment', AllpaymentPageAction::class)->name('allpayment.page'); - Route::post('allpayment', AllpaymentStoreAction::class)->name('allpayment.store'); Route::resource('subscription', SubscriptionController::class); Route::get('/sendpayment', [SendpaymentController::class, 'create'])->name('sendpayment.create'); Route::get('/sendpayment/pdf', [SendpaymentController::class, 'send'])->name('sendpayment.pdf'); @@ -113,6 +112,10 @@ Route::group(['middleware' => 'auth:web'], function (): void { Route::patch('/payment/{payment}', PaymentUpdateAction::class)->name('payment.update'); Route::delete('/payment/{payment}', PaymentDestroyAction::class)->name('payment.destroy'); + // -------------------------------- allpayment --------------------------------- + Route::get('allpayment', AllpaymentPageAction::class)->name('allpayment.page'); + Route::post('allpayment', MassStoreAction::class)->name('allpayment.store'); + // ---------------------------------- invoice ---------------------------------- Route::post('/invoice', InvoiceStoreAction::class)->name('invoice.store'); diff --git a/tests/Feature/Invoice/InvoiceRequestFactory.php b/tests/Feature/Invoice/InvoiceRequestFactory.php index a50417cf..ddef2368 100644 --- a/tests/Feature/Invoice/InvoiceRequestFactory.php +++ b/tests/Feature/Invoice/InvoiceRequestFactory.php @@ -13,8 +13,6 @@ class InvoiceRequestFactory extends RequestFactory return [ 'to' => ReceiverRequestFactory::new(), 'greeting' => 'Hallo Familie', - 'intro' => 'Hiermit stellen wir ihnen den Beitrag in Rechnung.', - 'outro' => 'Das ist die Rechnung', 'positions' => [] ]; } diff --git a/tests/Feature/Invoice/InvoiceStoreActionTest.php b/tests/Feature/Invoice/InvoiceStoreActionTest.php index a55abb34..6fb255dd 100644 --- a/tests/Feature/Invoice/InvoiceStoreActionTest.php +++ b/tests/Feature/Invoice/InvoiceStoreActionTest.php @@ -27,8 +27,6 @@ class InvoiceStoreActionTest extends TestCase ->status(InvoiceStatus::PAID) ->state([ 'greeting' => 'Hallo Familie', - 'intro' => 'Hiermit stellen wir ihnen den Beitrag in Rechnung.', - 'outro' => 'Das ist die Rechnung', ]) ->create() ); @@ -36,8 +34,6 @@ class InvoiceStoreActionTest extends TestCase $response->assertOk(); $this->assertDatabaseHas('invoices', [ 'greeting' => 'Hallo Familie', - 'intro' => 'Hiermit stellen wir ihnen den Beitrag in Rechnung.', - 'outro' => 'Das ist die Rechnung', 'status' => InvoiceStatus::PAID->value, ]); $invoice = Invoice::firstWhere('greeting', 'Hallo Familie'); diff --git a/tests/Feature/Invoice/MassStoreActionTest.php b/tests/Feature/Invoice/MassStoreActionTest.php new file mode 100644 index 00000000..6683f32d --- /dev/null +++ b/tests/Feature/Invoice/MassStoreActionTest.php @@ -0,0 +1,104 @@ +login()->loginNami()->withoutExceptionHandling(); + } + + public function testItDoesntCreatePaymentsWithoutSubscription(): void + { + Member::factory()->defaults()->emailBillKind()->create(['subscription_id' => null]); + + $this->postJson(route('allpayment.store'), [ + 'year' => now()->addYear()->year, + ])->assertOk(); + + $this->assertDatabaseEmpty('invoices'); + } + + public function testItDoesntCreatePaymentWithoutBillKind(): void + { + Member::factory()->defaults()->create(); + + $this->postJson(route('allpayment.store'), [ + 'year' => now()->addYear()->year, + ])->assertOk(); + + $this->assertDatabaseEmpty('invoices'); + } + + public function testItCreatesPayments(): void + { + $member = Member::factory()->defaults() + ->for(Subscription::factory()->children([ + new Child('beitrag {name}', 4466), + new Child('beitrag2 für {name} für {year}', 2290), + ]))->emailBillKind()->create(['firstname' => 'Max', 'lastname' => 'Muster', 'address' => 'Maxstr 4', 'zip' => '33445', 'location' => 'Solingen']); + + $this->postJson(route('allpayment.store'), [ + 'year' => now()->addYear()->year, + ])->assertOk(); + + $invoice = Invoice::first(); + $this->assertNotNull($invoice); + $this->assertEquals([ + 'name' => 'Familie Muster', + 'address' => 'Maxstr 4', + 'zip' => '33445', + 'location' => 'Solingen', + ], $invoice->to); + $this->assertDatabaseHas('invoice_positions', [ + 'invoice_id' => $invoice->id, + 'member_id' => $member->id, + 'price' => 4466, + 'description' => 'beitrag Max Muster' + ]); + $this->assertDatabaseHas('invoice_positions', [ + 'invoice_id' => $invoice->id, + 'member_id' => $member->id, + 'price' => 2290, + 'description' => 'beitrag2 für Max Muster für ' . now()->addYear()->year + ]); + } + + public function testItCreatesOneInvoiceForFamilyMember(): void + { + $subscription = Subscription::factory()->children([new Child('beitrag {name}', 4466)])->create(); + $member = Member::factory()->defaults()->for($subscription)->emailBillKind()->create(['firstname' => 'Max', 'lastname' => 'Muster']); + Member::factory()->defaults()->for($subscription)->sameFamilyAs($member)->emailBillKind()->create(['firstname' => 'Jane']); + + $this->postJson(route('allpayment.store'), ['year' => now()->addYear()->year])->assertOk(); + + $this->assertDatabaseCount('invoices', 1); + $this->assertDatabaseCount('invoice_positions', 2); + $this->assertDatabaseHas('invoice_positions', ['description' => 'beitrag Max Muster']); + $this->assertDatabaseHas('invoice_positions', ['description' => 'beitrag Jane Muster']); + } + + public function testItSeparatesBillKinds(): void + { + $subscription = Subscription::factory()->children([new Child('beitrag {name]', 4466)])->create(); + $member = Member::factory()->defaults()->for($subscription)->emailBillKind()->create(); + Member::factory()->defaults()->for($subscription)->sameFamilyAs($member)->postBillKind()->create(); + + $this->postJson(route('allpayment.store'), ['year' => now()->addYear()->year])->assertOk(); + + $this->assertDatabaseCount('invoices', 2); + $this->assertDatabaseCount('invoice_positions', 2); + } +} diff --git a/tests/Feature/Payment/AllpaymentTest.php b/tests/Feature/Payment/AllpaymentTest.php deleted file mode 100644 index e66070b6..00000000 --- a/tests/Feature/Payment/AllpaymentTest.php +++ /dev/null @@ -1,145 +0,0 @@ -login()->loginNami(); - } - - public function testItDoesntCreatePaymentsWithoutSubscription(): void - { - $member = Member::factory()->defaults()->emailBillKind()->create(); - $member->update(['subscription_id' => null]); - - $response = $this->from('/allpayment/create')->post('allpayment', [ - 'year' => now()->addYear()->year, - ]); - - $response->assertRedirect('/allpayment/create'); - $this->assertEmpty($member->payments()->get()); - } - - public function testItDoesntCreatePaymentWithoutBillKind(): void - { - $member = Member::factory()->defaults()->create(); - - $response = $this->from('/allpayment/create')->post('allpayment', [ - 'year' => now()->addYear()->year, - ]); - - $response->assertRedirect('/allpayment/create'); - $this->assertEmpty($member->payments()->get()); - } - - public function testItCreatesPayments(): void - { - $member = Member::factory()->defaults()->emailBillKind()->create(); - - $response = $this->from('/allpayment/create')->post('allpayment', [ - 'year' => now()->addYear()->year, - 'for_promise' => false, - ]); - - $response->assertRedirect('/allpayment/create'); - $this->assertDatabaseHas('payments', [ - 'member_id' => $member->id, - 'nr' => now()->addYear()->year, - 'subscription_id' => $member->subscription->id, - 'status_id' => Status::first()->id, - ]); - } - - public function testItCreatesPromisePayments(): void - { - $member = Member::factory() - ->defaults() - ->emailBillKind() - ->has(Membership::factory()->in('€ Mitglied', 123, 'Rover', 124)->promise(now()->subYear()->startOfYear())) - ->create(); - - $subscription = Subscription::factory()->forPromise()->create(); - - $this->from('/allpayment/create')->post('allpayment', [ - 'year' => now()->year, - 'for_promise' => true, - ]); - - $this->assertDatabaseHas('payments', [ - 'member_id' => $member->id, - 'nr' => 'Rover '.now()->subYear()->year, - 'subscription_id' => $subscription->id, - 'status_id' => Status::first()->id, - ]); - } - - public function testItDoesntCreatePromisePaymentsWhenPromiseIsOver(): void - { - $member = Member::factory() - ->defaults() - ->emailBillKind() - ->has(Membership::factory()->in('€ Mitglied', 123, 'Rover', 124)->promise(now()->subYears(2)->startOfYear())) - ->create(); - - $subscription = Subscription::factory()->forPromise()->create(); - - $this->from('/allpayment/create')->post('allpayment', [ - 'year' => now()->year, - 'for_promise' => true, - ]); - - $this->assertDatabaseMissing('payments', [ - 'subscription_id' => $subscription->id, - ]); - } - - public function testItDoesntCreatePromisePaymentsWhenUserAlreadyHasPayment(): void - { - $subscription = Subscription::factory()->forPromise()->create(); - - $member = Member::factory() - ->defaults() - ->emailBillKind() - ->has(Membership::factory()->in('€ Mitglied', 123, 'Rover', 124)->promise(now()->subYear()->startOfYear())) - ->has(Payment::factory()->notPaid()->nr('Rover '.now()->subYear()->year)->for($subscription)) - ->create(); - - $this->from('/allpayment/create')->post('allpayment', [ - 'year' => now()->year, - 'for_promise' => true, - ]); - - $this->assertCount(2, $member->payments); - } - - public function testItDoesntCreatePromisePaymentsWhenNoSubscriptionFound(): void - { - $member = Member::factory() - ->defaults() - ->emailBillKind() - ->has(Membership::factory()->in('€ Mitglied', 123, 'Rover', 124)->promise(now()->subYear()->startOfYear())) - ->has(Payment::factory()->notPaid()->nr('Rover '.now()->subYear()->year)) - ->create(); - - $this->from('/allpayment/create')->post('allpayment', [ - 'year' => now()->year, - 'for_promise' => true, - ]); - - $this->assertCount(2, $member->payments); - } -}