Mod AllPayment so that it creates an invoice

This commit is contained in:
Philipp Lang 2023-12-16 00:16:07 +01:00
parent 5c40b4e64d
commit 15b62e59fc
12 changed files with 186 additions and 252 deletions

View File

@ -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',

View File

@ -0,0 +1,50 @@
<?php
namespace App\Invoice\Actions;
use App\Invoice\Models\Invoice;
use App\Member\Member;
use Illuminate\Http\JsonResponse;
use Lorisleiva\Actions\ActionRequest;
use Lorisleiva\Actions\Concerns\AsAction;
class MassStoreAction
{
use AsAction;
/**
* @return array<string, string>
*/
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([]);
}
}

View File

@ -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,
]);
}
}

View File

@ -357,18 +357,6 @@ class Member extends Model implements Geolocatable
return $query->where('bill_kind', '!=', null)->where('subscription_id', '!=', null);
}
/**
* @param Builder<self> $query
*
* @return Builder<self>
*/
public function scopeWhereNoPayment(Builder $query, int $year): Builder
{
return $query->whereDoesntHave('payments', function (Builder $q) use ($year) {
$q->where('nr', '=', $year);
});
}
/**
* @param Builder<self> $query
*

View File

@ -1,83 +0,0 @@
<?php
namespace App\Payment\Actions;
use App\Member\Member;
use App\Member\Membership;
use App\Payment\Status;
use App\Payment\Subscription;
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Http\RedirectResponse;
use Lorisleiva\Actions\ActionRequest;
use Lorisleiva\Actions\Concerns\AsAction;
class AllpaymentStoreAction
{
use AsAction;
/**
* @return array<string, string>
*/
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<int, Membership>
*/
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');
}
}

View File

@ -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,
]);
}
}

View File

@ -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();
});

View File

@ -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');

View File

@ -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' => []
];
}

View File

@ -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');

View File

@ -0,0 +1,104 @@
<?php
namespace Tests\Feature\Invoice;
use App\Invoice\Models\Invoice;
use App\Member\Member;
use App\Payment\Subscription;
use Illuminate\Foundation\Testing\DatabaseTransactions;
use Tests\RequestFactories\Child;
use Tests\TestCase;
class MassStoreActionTest extends TestCase
{
use DatabaseTransactions;
public function setUp(): void
{
parent::setUp();
$this->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);
}
}

View File

@ -1,145 +0,0 @@
<?php
namespace Tests\Feature\Payment;
use App\Member\Member;
use App\Member\Membership;
use App\Payment\Payment;
use App\Payment\Status;
use App\Payment\Subscription;
use Illuminate\Foundation\Testing\DatabaseTransactions;
use Tests\TestCase;
class AllpaymentTest extends TestCase
{
use DatabaseTransactions;
public function setUp(): void
{
parent::setUp();
$this->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);
}
}