Add DisplayPdfAction for invoices
This commit is contained in:
parent
551c658fa3
commit
b0534279b6
|
@ -0,0 +1,21 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Invoice\Actions;
|
||||||
|
|
||||||
|
use App\Invoice\BillDocument;
|
||||||
|
use App\Invoice\Models\Invoice;
|
||||||
|
use App\Payment\Payment;
|
||||||
|
use Illuminate\Http\Response;
|
||||||
|
use Lorisleiva\Actions\Concerns\AsAction;
|
||||||
|
use Zoomyboy\Tex\BaseCompiler;
|
||||||
|
use Zoomyboy\Tex\Tex;
|
||||||
|
|
||||||
|
class DisplayPdfAction
|
||||||
|
{
|
||||||
|
use AsAction;
|
||||||
|
|
||||||
|
public function handle(Invoice $invoice): BaseCompiler|Response
|
||||||
|
{
|
||||||
|
return Tex::compile(BillDocument::fromInvoice($invoice));
|
||||||
|
}
|
||||||
|
}
|
|
@ -5,7 +5,7 @@ namespace App\Invoice;
|
||||||
use App\Payment\Payment;
|
use App\Payment\Payment;
|
||||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||||
|
|
||||||
class BillDocument extends Invoice
|
class BillDocument extends InvoiceDocument
|
||||||
{
|
{
|
||||||
public function linkLabel(): string
|
public function linkLabel(): string
|
||||||
{
|
{
|
||||||
|
|
|
@ -2,17 +2,16 @@
|
||||||
|
|
||||||
namespace App\Invoice;
|
namespace App\Invoice;
|
||||||
|
|
||||||
use App\Member\Member;
|
use App\Invoice\Models\Invoice;
|
||||||
use App\Payment\Payment;
|
use App\Payment\Payment;
|
||||||
use Exception;
|
use Exception;
|
||||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||||
use Illuminate\Support\Collection;
|
|
||||||
use Illuminate\Support\Str;
|
use Illuminate\Support\Str;
|
||||||
use Zoomyboy\Tex\Document;
|
use Zoomyboy\Tex\Document;
|
||||||
use Zoomyboy\Tex\Engine;
|
use Zoomyboy\Tex\Engine;
|
||||||
use Zoomyboy\Tex\Template;
|
use Zoomyboy\Tex\Template;
|
||||||
|
|
||||||
abstract class Invoice extends Document
|
abstract class InvoiceDocument extends Document
|
||||||
{
|
{
|
||||||
abstract public function getSubject(): string;
|
abstract public function getSubject(): string;
|
||||||
abstract public function view(): string;
|
abstract public function view(): string;
|
||||||
|
@ -39,33 +38,28 @@ abstract class Invoice extends Document
|
||||||
* @param array<string, string> $positions
|
* @param array<string, string> $positions
|
||||||
*/
|
*/
|
||||||
public function __construct(
|
public function __construct(
|
||||||
public string $familyName,
|
public string $toName,
|
||||||
public string $singleName,
|
public string $toAddress,
|
||||||
public string $address,
|
public string $toZip,
|
||||||
public string $zip,
|
public string $toLocation,
|
||||||
public string $location,
|
public string $greeting,
|
||||||
public array $positions,
|
public array $positions,
|
||||||
public string $usage,
|
public string $usage,
|
||||||
public ?string $email,
|
|
||||||
) {
|
) {
|
||||||
$this->until = now()->addWeeks(2)->format('d.m.Y');
|
$this->until = now()->addWeeks(2)->format('d.m.Y');
|
||||||
$this->filename = Str::slug("{$this->getSubject()} für {$familyName}");
|
$this->filename = Str::slug("{$this->getSubject()} für {$toName}");
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
public static function fromInvoice(Invoice $invoice): self
|
||||||
* @param Collection<(int|string), Member> $members
|
|
||||||
*/
|
|
||||||
public static function fromMembers(Collection $members): self
|
|
||||||
{
|
{
|
||||||
return static::withoutMagicalCreationFrom([
|
return static::withoutMagicalCreationFrom([
|
||||||
'familyName' => $members->first()->lastname,
|
'toName' => $invoice->to['name'],
|
||||||
'singleName' => $members->first()->lastname,
|
'toAddress' => $invoice->to['address'],
|
||||||
'address' => $members->first()->address,
|
'toZip' => $invoice->to['zip'],
|
||||||
'zip' => $members->first()->zip,
|
'toLocation' => $invoice->to['location'],
|
||||||
'location' => $members->first()->location,
|
'greeting' => $invoice->greeting,
|
||||||
'email' => $members->first()->email_parents ?: $members->first()->email,
|
'positions' => static::renderPositions($invoice),
|
||||||
'positions' => static::renderPositions($members),
|
'usage' => $invoice->usage,
|
||||||
'usage' => "Mitgliedsbeitrag für {$members->first()->lastname}",
|
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -116,26 +110,11 @@ abstract class Invoice extends Document
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param Collection<(int|string), Member> $members
|
|
||||||
*
|
|
||||||
* @return array<string, string>
|
* @return array<string, string>
|
||||||
*/
|
*/
|
||||||
public static function renderPositions(Collection $members): array
|
public static function renderPositions(Invoice $invoice): array
|
||||||
{
|
{
|
||||||
/** @var array<string, string> */
|
return $invoice->positions->mapWithKeys(fn ($position) => [$position->description => static::number($position->price)])->toArray();
|
||||||
$result = [];
|
|
||||||
|
|
||||||
foreach ($members->pluck('payments')->flatten(1) as $payment) {
|
|
||||||
if ($payment->subscription->split) {
|
|
||||||
foreach ($payment->subscription->children as $child) {
|
|
||||||
$result["{$payment->subscription->name} ({$child->name}) {$payment->nr} für {$payment->member->firstname} {$payment->member->lastname}"] = static::number($child->amount);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
$result["{$payment->subscription->name} {$payment->nr} für {$payment->member->firstname} {$payment->member->lastname}"] = static::number($payment->subscription->getAmount());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return $result;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public static function number(int $number): string
|
public static function number(int $number): string
|
|
@ -1,26 +0,0 @@
|
||||||
<?php
|
|
||||||
|
|
||||||
namespace App\Payment\Actions;
|
|
||||||
|
|
||||||
use App\Invoice\BillDocument;
|
|
||||||
use App\Payment\Payment;
|
|
||||||
use Illuminate\Http\Response;
|
|
||||||
use Lorisleiva\Actions\Concerns\AsAction;
|
|
||||||
use Zoomyboy\Tex\BaseCompiler;
|
|
||||||
use Zoomyboy\Tex\Tex;
|
|
||||||
|
|
||||||
class DisplayPdfAction
|
|
||||||
{
|
|
||||||
use AsAction;
|
|
||||||
|
|
||||||
public function handle(Payment $payment): BaseCompiler|Response
|
|
||||||
{
|
|
||||||
if (null === $payment->invoice_data) {
|
|
||||||
return response()->noContent();
|
|
||||||
}
|
|
||||||
|
|
||||||
$invoice = BillDocument::from($payment->invoice_data);
|
|
||||||
|
|
||||||
return Tex::compile($invoice);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -27,7 +27,8 @@ class InvoiceFactory extends Factory
|
||||||
'greeting' => $this->faker->words(4, true),
|
'greeting' => $this->faker->words(4, true),
|
||||||
'to' => ReceiverRequestFactory::new()->create(),
|
'to' => ReceiverRequestFactory::new()->create(),
|
||||||
'status' => InvoiceStatus::NEW->value,
|
'status' => InvoiceStatus::NEW->value,
|
||||||
'via' => BillKind::POST->value
|
'via' => BillKind::POST->value,
|
||||||
|
'usage' => $this->faker->words(4, true),
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -20,6 +20,7 @@ return new class extends Migration
|
||||||
$table->string('status');
|
$table->string('status');
|
||||||
$table->date('sent_at')->nullable();
|
$table->date('sent_at')->nullable();
|
||||||
$table->string('via');
|
$table->string('via');
|
||||||
|
$table->string('usage');
|
||||||
$table->timestamps();
|
$table->timestamps();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -11,10 +11,10 @@
|
||||||
\setkomavar{fromlogo}{\includegraphics[width=2cm]{logo.png}} % stammeslogo
|
\setkomavar{fromlogo}{\includegraphics[width=2cm]{logo.png}} % stammeslogo
|
||||||
|
|
||||||
\begin{document}
|
\begin{document}
|
||||||
\begin{letter}{Familie <<< $familyName >>>\\<<< $address >>>\\<<< $zip >>> <<< $location >>>}
|
\begin{letter}{<<< $toName >>>\\<<< $toAddress >>>\\<<< $toZip >>> <<< $toLocation >>>}
|
||||||
\sffamily
|
\sffamily
|
||||||
\gdef\TotalHT{0}
|
\gdef\TotalHT{0}
|
||||||
\opening{Liebe Familie <<< $familyName >>>,}
|
\opening{<<< $greeting >>>,}
|
||||||
|
|
||||||
Hiermit stellen wir Ihnen den aktuellen Mitgliedsbeitrag für den \usekomavar*{fromname} und die DPSG in Rechnung. Dieser setzt sich wie folgt zusammen:
|
Hiermit stellen wir Ihnen den aktuellen Mitgliedsbeitrag für den \usekomavar*{fromname} und die DPSG in Rechnung. Dieser setzt sich wie folgt zusammen:
|
||||||
|
|
||||||
|
|
|
@ -25,6 +25,7 @@ use App\Initialize\Actions\InitializeFormAction;
|
||||||
use App\Initialize\Actions\NamiGetSearchLayerAction;
|
use App\Initialize\Actions\NamiGetSearchLayerAction;
|
||||||
use App\Initialize\Actions\NamiLoginCheckAction;
|
use App\Initialize\Actions\NamiLoginCheckAction;
|
||||||
use App\Initialize\Actions\NamiSearchAction;
|
use App\Initialize\Actions\NamiSearchAction;
|
||||||
|
use App\Invoice\Actions\DisplayPdfAction;
|
||||||
use App\Invoice\Actions\InvoiceDestroyAction;
|
use App\Invoice\Actions\InvoiceDestroyAction;
|
||||||
use App\Invoice\Actions\InvoiceIndexAction;
|
use App\Invoice\Actions\InvoiceIndexAction;
|
||||||
use App\Invoice\Actions\InvoiceUpdateAction;
|
use App\Invoice\Actions\InvoiceUpdateAction;
|
||||||
|
@ -50,11 +51,6 @@ use App\Membership\Actions\MembershipDestroyAction;
|
||||||
use App\Membership\Actions\MembershipStoreAction;
|
use App\Membership\Actions\MembershipStoreAction;
|
||||||
use App\Membership\Actions\MembershipUpdateAction;
|
use App\Membership\Actions\MembershipUpdateAction;
|
||||||
use App\Membership\Actions\StoreForGroupAction;
|
use App\Membership\Actions\StoreForGroupAction;
|
||||||
use App\Payment\Actions\DisplayPdfAction;
|
|
||||||
use App\Payment\Actions\IndexAction as PaymentIndexAction;
|
|
||||||
use App\Payment\Actions\PaymentDestroyAction;
|
|
||||||
use App\Payment\Actions\PaymentStoreAction;
|
|
||||||
use App\Payment\Actions\PaymentUpdateAction;
|
|
||||||
use App\Payment\SendpaymentController;
|
use App\Payment\SendpaymentController;
|
||||||
use App\Payment\SubscriptionController;
|
use App\Payment\SubscriptionController;
|
||||||
|
|
||||||
|
@ -107,9 +103,6 @@ Route::group(['middleware' => 'auth:web'], function (): void {
|
||||||
// ----------------------------------- group -----------------------------------
|
// ----------------------------------- group -----------------------------------
|
||||||
Route::get('/group', ListAction::class)->name('group.index');
|
Route::get('/group', ListAction::class)->name('group.index');
|
||||||
|
|
||||||
// ---------------------------------- payment ----------------------------------
|
|
||||||
Route::get('/payment/{payment}/pdf', DisplayPdfAction::class)->name('payment.pdf');
|
|
||||||
|
|
||||||
// -------------------------------- allpayment ---------------------------------
|
// -------------------------------- allpayment ---------------------------------
|
||||||
Route::post('/invoice/mass-store', MassStoreAction::class)->name('invoice.mass-store');
|
Route::post('/invoice/mass-store', MassStoreAction::class)->name('invoice.mass-store');
|
||||||
|
|
||||||
|
@ -118,6 +111,7 @@ Route::group(['middleware' => 'auth:web'], function (): void {
|
||||||
Route::post('/invoice', InvoiceStoreAction::class)->name('invoice.store');
|
Route::post('/invoice', InvoiceStoreAction::class)->name('invoice.store');
|
||||||
Route::patch('/invoice/{invoice}', InvoiceUpdateAction::class)->name('invoice.update');
|
Route::patch('/invoice/{invoice}', InvoiceUpdateAction::class)->name('invoice.update');
|
||||||
Route::delete('/invoice/{invoice}', InvoiceDestroyAction::class)->name('invoice.destroy');
|
Route::delete('/invoice/{invoice}', InvoiceDestroyAction::class)->name('invoice.destroy');
|
||||||
|
Route::get('/invoice/{invoice}/pdf', DisplayPdfAction::class)->name('invoice.pdf');
|
||||||
|
|
||||||
|
|
||||||
// ----------------------------- invoice-position ------------------------------
|
// ----------------------------- invoice-position ------------------------------
|
||||||
|
|
|
@ -0,0 +1,35 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Tests\Feature\Invoice;
|
||||||
|
|
||||||
|
use App\Invoice\BillDocument;
|
||||||
|
use App\Invoice\BillKind;
|
||||||
|
use App\Invoice\Models\Invoice;
|
||||||
|
use App\Invoice\Models\InvoicePosition;
|
||||||
|
use Illuminate\Foundation\Testing\DatabaseTransactions;
|
||||||
|
use Tests\TestCase;
|
||||||
|
use Zoomyboy\Tex\Tex;
|
||||||
|
|
||||||
|
class ShowPdfTest extends TestCase
|
||||||
|
{
|
||||||
|
|
||||||
|
use DatabaseTransactions;
|
||||||
|
|
||||||
|
public function testItShowsAnInvoiceAsPdf(): void
|
||||||
|
{
|
||||||
|
Tex::spy();
|
||||||
|
$this->login()->loginNami();
|
||||||
|
$invoice = Invoice::factory()
|
||||||
|
->to(ReceiverRequestFactory::new()->name('Familie Lala'))
|
||||||
|
->has(InvoicePosition::factory()->withMember()->description('Beitrag12'), 'positions')
|
||||||
|
->via(BillKind::EMAIL)
|
||||||
|
->create();
|
||||||
|
|
||||||
|
$this->get(route('invoice.pdf', ['invoice' => $invoice]))
|
||||||
|
->assertOk()
|
||||||
|
->assertPdfPageCount(1)
|
||||||
|
->assertPdfName('rechnung-fur-familie-lala.pdf');
|
||||||
|
|
||||||
|
Tex::assertCompiled(BillDocument::class, fn ($document) => $document->hasAllContent(['Beitrag12', 'Familie Lala']));
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,52 +0,0 @@
|
||||||
<?php
|
|
||||||
|
|
||||||
namespace Tests\Feature\Payment;
|
|
||||||
|
|
||||||
use App\Invoice\BillDocument;
|
|
||||||
use App\Invoice\DocumentFactory;
|
|
||||||
use App\Member\Member;
|
|
||||||
use App\Payment\Payment;
|
|
||||||
use Illuminate\Foundation\Testing\DatabaseTransactions;
|
|
||||||
use Illuminate\Support\Collection;
|
|
||||||
use Tests\RequestFactories\Child;
|
|
||||||
use Tests\TestCase;
|
|
||||||
|
|
||||||
class PaymentPdfTest extends TestCase
|
|
||||||
{
|
|
||||||
|
|
||||||
use DatabaseTransactions;
|
|
||||||
|
|
||||||
public function testItShowsAnInvoiceAsPdf(): void
|
|
||||||
{
|
|
||||||
$this->login()->loginNami();
|
|
||||||
$member = Member::factory()
|
|
||||||
->defaults()
|
|
||||||
->has(Payment::factory()->notPaid()->nr('1997')->subscription('tollerbeitrag', [
|
|
||||||
new Child('a', 5400),
|
|
||||||
]))
|
|
||||||
->emailBillKind()
|
|
||||||
->create(['firstname' => 'Lah', 'lastname' => 'Mom', 'email' => 'peter@example.com']);
|
|
||||||
/** @var Collection<(int|string), Member> */
|
|
||||||
$members = collect([$member]);
|
|
||||||
app(DocumentFactory::class)->afterSingle(BillDocument::fromMembers($members), $members);
|
|
||||||
|
|
||||||
$response = $this->get(route('payment.pdf', ['payment' => $member->payments->first()]));
|
|
||||||
$response->assertOk();
|
|
||||||
$this->assertPdfPageCount(1, $response->getFile());
|
|
||||||
}
|
|
||||||
|
|
||||||
public function testItReturnsNoPdfWhenPaymentDoesntHaveInvoiceData(): void
|
|
||||||
{
|
|
||||||
$this->login()->loginNami();
|
|
||||||
$member = Member::factory()
|
|
||||||
->defaults()
|
|
||||||
->has(Payment::factory()->notPaid()->nr('1997')->subscription('tollerbeitrag', [
|
|
||||||
new Child('a', 5400),
|
|
||||||
]))
|
|
||||||
->emailBillKind()
|
|
||||||
->create(['firstname' => 'Lah', 'lastname' => 'Mom', 'email' => 'peter@example.com']);
|
|
||||||
|
|
||||||
$response = $this->get(route('payment.pdf', ['payment' => $member->payments->first()]));
|
|
||||||
$response->assertStatus(204);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -111,16 +111,6 @@ abstract class TestCase extends BaseTestCase
|
||||||
return $this;
|
return $this;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function assertPdfPageCount(int $pageCount, File $file): void
|
|
||||||
{
|
|
||||||
$this->assertTrue(file_exists($file->getPathname()));
|
|
||||||
exec('pdfinfo ' . escapeshellarg($file->getPathname()) . ' | grep ^Pages | sed "s/Pages:\s*//"', $output, $returnVar);
|
|
||||||
|
|
||||||
$this->assertSame(0, $returnVar, 'Failed to get Pages of PDF File ' . $file->getPathname());
|
|
||||||
$this->assertCount(1, $output, 'Failed to parse output format of pdfinfo');
|
|
||||||
$this->assertEquals($pageCount, $output[0]);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function initInertiaTestcase(): void
|
public function initInertiaTestcase(): void
|
||||||
{
|
{
|
||||||
TestResponse::macro('assertInertiaPath', function ($path, $value) {
|
TestResponse::macro('assertInertiaPath', function ($path, $value) {
|
||||||
|
@ -132,5 +122,27 @@ abstract class TestCase extends BaseTestCase
|
||||||
$json->assertPath($path, $value);
|
$json->assertPath($path, $value);
|
||||||
return $this;
|
return $this;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
TestResponse::macro('assertPdfPageCount', function (int $count) {
|
||||||
|
/** @var TestResponse */
|
||||||
|
$response = $this;
|
||||||
|
$file = $response->getFile();
|
||||||
|
Assert::assertTrue(file_exists($file->getPathname()));
|
||||||
|
exec('pdfinfo ' . escapeshellarg($file->getPathname()) . ' | grep ^Pages | sed "s/Pages:\s*//"', $output, $returnVar);
|
||||||
|
|
||||||
|
Assert::assertSame(0, $returnVar, 'Failed to get Pages of PDF File ' . $file->getPathname());
|
||||||
|
Assert::assertCount(1, $output, 'Failed to parse output format of pdfinfo');
|
||||||
|
Assert::assertEquals($count, $output[0]);
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
});
|
||||||
|
|
||||||
|
TestResponse::macro('assertPdfName', function (string $filename) {
|
||||||
|
/** @var TestResponse */
|
||||||
|
$response = $this;
|
||||||
|
Assert::assertEquals($filename, $response->getFile()->getFilename());
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue