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\CourseIndexAction;
|
||||
use App\Course\Actions\CourseStoreAction;
|
||||
use App\Invoice\Actions\InvoiceStoreAction;
|
||||
use App\Course\Actions\CourseUpdateAction;
|
||||
use App\Dashboard\Actions\IndexAction as DashboardIndexAction;
|
||||
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::delete('/payment/{payment}', PaymentDestroyAction::class)->name('payment.destroy');
|
||||
|
||||
// ---------------------------------- invoice ----------------------------------
|
||||
Route::post('/invoice', InvoiceStoreAction::class)->name('invoice.store');
|
||||
|
||||
// --------------------------------- membership --------------------------------
|
||||
Route::get('/member/{member}/membership', MembershipIndexAction::class)->name('member.membership.index');
|
||||
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\Subscription;
|
||||
use Illuminate\Foundation\Testing\DatabaseTransactions;
|
||||
use Illuminate\Support\Collection;
|
||||
use Tests\RequestFactories\Child;
|
||||
use Tests\TestCase;
|
||||
|
||||
|
@ -54,6 +55,7 @@ class IndexTest extends TestCase
|
|||
]))
|
||||
->defaults()->create();
|
||||
|
||||
/** @var Collection<int|string, Member> */
|
||||
$members = collect([$member]);
|
||||
app(DocumentFactory::class)->afterSingle(BillDocument::fromMembers($members), $members);
|
||||
|
||||
|
|
|
@ -45,7 +45,6 @@ class PaymentPdfTest extends TestCase
|
|||
]))
|
||||
->emailBillKind()
|
||||
->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->assertStatus(204);
|
||||
|
|
Loading…
Reference in New Issue