Add InvoiceStoreAction
continuous-integration/drone/push Build is failing
Details
continuous-integration/drone/push Build is failing
Details
This commit is contained in:
parent
ad8511874d
commit
5c40b4e64d
|
@ -0,0 +1,59 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Invoice\Actions;
|
||||||
|
|
||||||
|
use App\Invoice\Enums\InvoiceStatus;
|
||||||
|
use Lorisleiva\Actions\ActionRequest;
|
||||||
|
use Lorisleiva\Actions\Concerns\AsAction;
|
||||||
|
use App\Invoice\Models\Invoice;
|
||||||
|
use Illuminate\Validation\Rule;
|
||||||
|
|
||||||
|
class InvoiceStoreAction
|
||||||
|
{
|
||||||
|
use AsAction;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return array<string, string|array<int, string|Rule>>
|
||||||
|
*/
|
||||||
|
public function rules(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'status' => ['required', 'string', 'max:255', Rule::in(InvoiceStatus::values())],
|
||||||
|
'to' => 'array',
|
||||||
|
'to.address' => 'required|string|max:255',
|
||||||
|
'to.location' => 'required|string|max:255',
|
||||||
|
'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',
|
||||||
|
'positions.*.member_id' => 'required|exists:members,id',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return array<string, string>
|
||||||
|
*/
|
||||||
|
public function getValidationAttributes(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'to.address' => 'Adresse',
|
||||||
|
'to.name' => 'Name',
|
||||||
|
'to.zip' => 'PLZ',
|
||||||
|
'to.location' => 'Ort',
|
||||||
|
'status' => 'Status',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function handle(ActionRequest $request): void
|
||||||
|
{
|
||||||
|
$invoice = Invoice::create($request->safe()->except('positions'));
|
||||||
|
|
||||||
|
foreach ($request->validated('positions') as $position) {
|
||||||
|
$invoice->positions()->create($position);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,20 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Invoice\Enums;
|
||||||
|
|
||||||
|
use Illuminate\Support\Collection;
|
||||||
|
|
||||||
|
enum InvoiceStatus: string
|
||||||
|
{
|
||||||
|
case NEW = 'Neu';
|
||||||
|
case SENT = 'Rechnung gestellt';
|
||||||
|
case PAID = 'Rechnung beglichen';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return Collection<int, string>
|
||||||
|
*/
|
||||||
|
public static function values(): Collection
|
||||||
|
{
|
||||||
|
return collect(static::cases())->map(fn ($case) => $case->value);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,26 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Invoice\Models;
|
||||||
|
|
||||||
|
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||||
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||||
|
|
||||||
|
class Invoice extends Model
|
||||||
|
{
|
||||||
|
use HasFactory;
|
||||||
|
|
||||||
|
public $guarded = [];
|
||||||
|
|
||||||
|
public $casts = [
|
||||||
|
'to' => 'json',
|
||||||
|
];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return HasMany<InvoicePosition>
|
||||||
|
*/
|
||||||
|
public function positions(): HasMany
|
||||||
|
{
|
||||||
|
return $this->hasMany(InvoicePosition::class);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,13 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Invoice\Models;
|
||||||
|
|
||||||
|
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||||
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
|
||||||
|
class InvoicePosition extends Model
|
||||||
|
{
|
||||||
|
use HasFactory;
|
||||||
|
|
||||||
|
public $guarded = [];
|
||||||
|
}
|
|
@ -0,0 +1,46 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
|
||||||
|
return new class extends Migration
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Run the migrations.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function up()
|
||||||
|
{
|
||||||
|
Schema::create('invoices', function (Blueprint $table) {
|
||||||
|
$table->id();
|
||||||
|
$table->json('to');
|
||||||
|
$table->string('greeting');
|
||||||
|
$table->text('intro');
|
||||||
|
$table->text('outro');
|
||||||
|
$table->string('status');
|
||||||
|
$table->timestamps();
|
||||||
|
});
|
||||||
|
|
||||||
|
Schema::create('invoice_positions', function (Blueprint $table) {
|
||||||
|
$table->id();
|
||||||
|
$table->foreignId('invoice_id');
|
||||||
|
$table->string('description');
|
||||||
|
$table->foreignId('member_id');
|
||||||
|
$table->unsignedBigInteger('price');
|
||||||
|
$table->timestamps();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reverse the migrations.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function down()
|
||||||
|
{
|
||||||
|
Schema::dropIfExists('invoice_positions');
|
||||||
|
Schema::dropIfExists('invoices');
|
||||||
|
}
|
||||||
|
};
|
|
@ -15,6 +15,7 @@ use App\Contribution\Actions\ValidateAction as ContributionValidateAction;
|
||||||
use App\Course\Actions\CourseDestroyAction;
|
use App\Course\Actions\CourseDestroyAction;
|
||||||
use App\Course\Actions\CourseIndexAction;
|
use App\Course\Actions\CourseIndexAction;
|
||||||
use App\Course\Actions\CourseStoreAction;
|
use App\Course\Actions\CourseStoreAction;
|
||||||
|
use App\Invoice\Actions\InvoiceStoreAction;
|
||||||
use App\Course\Actions\CourseUpdateAction;
|
use App\Course\Actions\CourseUpdateAction;
|
||||||
use App\Dashboard\Actions\IndexAction as DashboardIndexAction;
|
use App\Dashboard\Actions\IndexAction as DashboardIndexAction;
|
||||||
use App\Efz\ShowEfzDocumentAction;
|
use App\Efz\ShowEfzDocumentAction;
|
||||||
|
@ -112,6 +113,9 @@ Route::group(['middleware' => 'auth:web'], function (): void {
|
||||||
Route::patch('/payment/{payment}', PaymentUpdateAction::class)->name('payment.update');
|
Route::patch('/payment/{payment}', PaymentUpdateAction::class)->name('payment.update');
|
||||||
Route::delete('/payment/{payment}', PaymentDestroyAction::class)->name('payment.destroy');
|
Route::delete('/payment/{payment}', PaymentDestroyAction::class)->name('payment.destroy');
|
||||||
|
|
||||||
|
// ---------------------------------- invoice ----------------------------------
|
||||||
|
Route::post('/invoice', InvoiceStoreAction::class)->name('invoice.store');
|
||||||
|
|
||||||
// --------------------------------- membership --------------------------------
|
// --------------------------------- membership --------------------------------
|
||||||
Route::get('/member/{member}/membership', MembershipIndexAction::class)->name('member.membership.index');
|
Route::get('/member/{member}/membership', MembershipIndexAction::class)->name('member.membership.index');
|
||||||
Route::post('/member/{member}/membership', MembershipStoreAction::class)->name('member.membership.store');
|
Route::post('/member/{member}/membership', MembershipStoreAction::class)->name('member.membership.store');
|
||||||
|
|
|
@ -0,0 +1,32 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Tests\Feature\Invoice;
|
||||||
|
|
||||||
|
use App\Member\Member;
|
||||||
|
use Worksome\RequestFactories\RequestFactory;
|
||||||
|
|
||||||
|
class InvoicePositionRequestFactory extends RequestFactory
|
||||||
|
{
|
||||||
|
public function definition(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'description' => 'Beitrag Abc',
|
||||||
|
'price' => 3250,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function description(string $description): self
|
||||||
|
{
|
||||||
|
return $this->state(['description' => $description]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function price(int $price): self
|
||||||
|
{
|
||||||
|
return $this->state(['price' => $price]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function member(Member $member): self
|
||||||
|
{
|
||||||
|
return $this->state(['member_id' => $member->id]);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,38 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Tests\Feature\Invoice;
|
||||||
|
|
||||||
|
use App\Invoice\Enums\InvoiceStatus;
|
||||||
|
use App\Member\Member;
|
||||||
|
use Worksome\RequestFactories\RequestFactory;
|
||||||
|
|
||||||
|
class InvoiceRequestFactory extends RequestFactory
|
||||||
|
{
|
||||||
|
public function definition(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'to' => ReceiverRequestFactory::new(),
|
||||||
|
'greeting' => 'Hallo Familie',
|
||||||
|
'intro' => 'Hiermit stellen wir ihnen den Beitrag in Rechnung.',
|
||||||
|
'outro' => 'Das ist die Rechnung',
|
||||||
|
'positions' => []
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function to(ReceiverRequestFactory $to): self
|
||||||
|
{
|
||||||
|
return $this->state(['to' => $to]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function status(InvoiceStatus $status): self
|
||||||
|
{
|
||||||
|
return $this->state(['status' => $status->value]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function position(InvoicePositionRequestFactory $factory): self
|
||||||
|
{
|
||||||
|
return $this->state(['positions' => [
|
||||||
|
$factory->create(),
|
||||||
|
]]);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,111 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Tests\Feature\Invoice;
|
||||||
|
|
||||||
|
use App\Invoice\Enums\InvoiceStatus;
|
||||||
|
use App\Invoice\Models\Invoice;
|
||||||
|
use App\Member\Member;
|
||||||
|
use Generator;
|
||||||
|
use Illuminate\Foundation\Testing\DatabaseTransactions;
|
||||||
|
use Tests\TestCase;
|
||||||
|
|
||||||
|
class InvoiceStoreActionTest extends TestCase
|
||||||
|
{
|
||||||
|
|
||||||
|
use DatabaseTransactions;
|
||||||
|
|
||||||
|
public function testItCanCreateAnInvoice(): void
|
||||||
|
{
|
||||||
|
$this->login()->loginNami()->withoutExceptionHandling();
|
||||||
|
$member = Member::factory()->defaults()->create();
|
||||||
|
|
||||||
|
$response = $this->postJson(
|
||||||
|
route('invoice.store'),
|
||||||
|
InvoiceRequestFactory::new()
|
||||||
|
->to(ReceiverRequestFactory::new()->name('Familie Blabla')->address('Musterstr 44')->zip('22222')->location('Solingen'))
|
||||||
|
->position(InvoicePositionRequestFactory::new()->description('Beitrag Abc')->price(3250)->member($member))
|
||||||
|
->status(InvoiceStatus::PAID)
|
||||||
|
->state([
|
||||||
|
'greeting' => 'Hallo Familie',
|
||||||
|
'intro' => 'Hiermit stellen wir ihnen den Beitrag in Rechnung.',
|
||||||
|
'outro' => 'Das ist die Rechnung',
|
||||||
|
])
|
||||||
|
->create()
|
||||||
|
);
|
||||||
|
|
||||||
|
$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');
|
||||||
|
$this->assertDatabaseHas('invoice_positions', [
|
||||||
|
'invoice_id' => $invoice->id,
|
||||||
|
'member_id' => $member->id,
|
||||||
|
'price' => 3250,
|
||||||
|
'description' => 'Beitrag Abc',
|
||||||
|
]);
|
||||||
|
$this->assertEquals([
|
||||||
|
'name' => 'Familie Blabla',
|
||||||
|
'address' => 'Musterstr 44',
|
||||||
|
'zip' => '22222',
|
||||||
|
'location' => 'Solingen',
|
||||||
|
], $invoice->to);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function validationDataProvider(): Generator
|
||||||
|
{
|
||||||
|
yield [
|
||||||
|
['to.address' => ''],
|
||||||
|
['to.address' => 'Adresse ist erforderlich.']
|
||||||
|
];
|
||||||
|
|
||||||
|
yield [
|
||||||
|
['to.name' => ''],
|
||||||
|
['to.name' => 'Name ist erforderlich.']
|
||||||
|
];
|
||||||
|
|
||||||
|
yield [
|
||||||
|
['to.location' => ''],
|
||||||
|
['to.location' => 'Ort ist erforderlich.']
|
||||||
|
];
|
||||||
|
|
||||||
|
yield [
|
||||||
|
['status' => ''],
|
||||||
|
['status' => 'Status ist erforderlich.']
|
||||||
|
];
|
||||||
|
|
||||||
|
yield [
|
||||||
|
['status' => 'lala'],
|
||||||
|
['status' => 'Der gewählte Wert für Status ist ungültig.']
|
||||||
|
];
|
||||||
|
|
||||||
|
yield [
|
||||||
|
['to.zip' => ''],
|
||||||
|
['to.zip' => 'PLZ ist erforderlich.']
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param array<string, mixed> $input
|
||||||
|
* @param array<string, string> $errors
|
||||||
|
* @dataProvider validationDataProvider
|
||||||
|
*/
|
||||||
|
public function testItValidatesInput(array $input, array $errors): void
|
||||||
|
{
|
||||||
|
$this->login()->loginNami();
|
||||||
|
|
||||||
|
$response = $this->postJson(
|
||||||
|
route('invoice.store'),
|
||||||
|
InvoiceRequestFactory::new()
|
||||||
|
->to(ReceiverRequestFactory::new())
|
||||||
|
->position(InvoicePositionRequestFactory::new()->member(Member::factory()->defaults()->create()))
|
||||||
|
->state($input)
|
||||||
|
->create()
|
||||||
|
);
|
||||||
|
|
||||||
|
$response->assertJsonValidationErrors($errors);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,38 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Tests\Feature\Invoice;
|
||||||
|
|
||||||
|
use Worksome\RequestFactories\RequestFactory;
|
||||||
|
|
||||||
|
class ReceiverRequestFactory extends RequestFactory
|
||||||
|
{
|
||||||
|
public function definition(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'name' => 'Familie Blabla',
|
||||||
|
'address' => 'Musterstr 44',
|
||||||
|
'zip' => '22222',
|
||||||
|
'location' => 'Solingen',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function name(string $name): self
|
||||||
|
{
|
||||||
|
return $this->state(['name' => $name]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function address(string $address): self
|
||||||
|
{
|
||||||
|
return $this->state(['address' => $address]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function zip(string $zip): self
|
||||||
|
{
|
||||||
|
return $this->state(['zip' => $zip]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function location(string $location): self
|
||||||
|
{
|
||||||
|
return $this->state(['location' => $location]);
|
||||||
|
}
|
||||||
|
}
|
|
@ -8,6 +8,7 @@ use App\Member\Member;
|
||||||
use App\Payment\Payment;
|
use App\Payment\Payment;
|
||||||
use App\Payment\Subscription;
|
use App\Payment\Subscription;
|
||||||
use Illuminate\Foundation\Testing\DatabaseTransactions;
|
use Illuminate\Foundation\Testing\DatabaseTransactions;
|
||||||
|
use Illuminate\Support\Collection;
|
||||||
use Tests\RequestFactories\Child;
|
use Tests\RequestFactories\Child;
|
||||||
use Tests\TestCase;
|
use Tests\TestCase;
|
||||||
|
|
||||||
|
@ -54,6 +55,7 @@ class IndexTest extends TestCase
|
||||||
]))
|
]))
|
||||||
->defaults()->create();
|
->defaults()->create();
|
||||||
|
|
||||||
|
/** @var Collection<int|string, Member> */
|
||||||
$members = collect([$member]);
|
$members = collect([$member]);
|
||||||
app(DocumentFactory::class)->afterSingle(BillDocument::fromMembers($members), $members);
|
app(DocumentFactory::class)->afterSingle(BillDocument::fromMembers($members), $members);
|
||||||
|
|
||||||
|
|
|
@ -45,7 +45,6 @@ class PaymentPdfTest extends TestCase
|
||||||
]))
|
]))
|
||||||
->emailBillKind()
|
->emailBillKind()
|
||||||
->create(['firstname' => 'Lah', 'lastname' => 'Mom', 'email' => 'peter@example.com']);
|
->create(['firstname' => 'Lah', 'lastname' => 'Mom', 'email' => 'peter@example.com']);
|
||||||
/** @var Collection<(int|string), Member> */
|
|
||||||
|
|
||||||
$response = $this->get(route('payment.pdf', ['payment' => $member->payments->first()]));
|
$response = $this->get(route('payment.pdf', ['payment' => $member->payments->first()]));
|
||||||
$response->assertStatus(204);
|
$response->assertStatus(204);
|
||||||
|
|
Loading…
Reference in New Issue