Mod AllPayment so that it creates an invoice
This commit is contained in:
parent
5c40b4e64d
commit
15b62e59fc
|
@ -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',
|
||||
|
|
|
@ -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([]);
|
||||
}
|
||||
}
|
|
@ -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,
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
*
|
||||
|
|
|
@ -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');
|
||||
}
|
||||
}
|
|
@ -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,
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
});
|
||||
|
|
|
@ -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');
|
||||
|
||||
|
|
|
@ -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' => []
|
||||
];
|
||||
}
|
||||
|
|
|
@ -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');
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue