Add payment_data for payments

This commit is contained in:
Philipp Lang 2023-11-30 23:54:16 +01:00
parent fc1b647b54
commit 566ed704a6
24 changed files with 880 additions and 963 deletions

View File

@ -5,6 +5,7 @@ namespace App\Invoice\Actions;
use App\Invoice\BillKind;
use App\Invoice\DocumentFactory;
use App\Invoice\Queries\BillKindQuery;
use App\Payment\Payment;
use App\Payment\PaymentMail;
use Illuminate\Support\Facades\Mail;
use Illuminate\Support\Facades\Storage;
@ -33,13 +34,13 @@ class InvoiceSendAction
public function handle(): int
{
foreach (app(DocumentFactory::class)->getTypes() as $type) {
$invoices = app(DocumentFactory::class)->invoiceCollection($type, new BillKindQuery(BillKind::EMAIL));
$memberCollection = (new BillKindQuery(BillKind::EMAIL))->type($type)->getMembers();
foreach ($invoices as $invoice) {
foreach ($memberCollection as $members) {
$invoice = $type::fromMembers($members);
$invoicePath = Storage::disk('temp')->path(Tex::compile($invoice)->storeIn('', 'temp'));
Mail::to($invoice->getRecipient())
->send(new PaymentMail($invoice, $invoicePath));
app(DocumentFactory::class)->afterSingle($invoice);
Mail::to($invoice->getRecipient())->send(new PaymentMail($invoice, $invoicePath));
app(DocumentFactory::class)->afterSingle($invoice, $members);
}
}

View File

@ -29,7 +29,10 @@ class BillDocument extends Invoice
public function afterSingle(Payment $payment): void
{
$payment->update(['status_id' => 2]);
$payment->update([
'invoice_data' => $this->toArray(),
'status_id' => 2,
]);
}
public function getMailSubject(): string

View File

@ -2,7 +2,7 @@
namespace App\Invoice;
use App\Invoice\Queries\InvoiceMemberQuery;
use App\Member\Member;
use Illuminate\Support\Collection;
class DocumentFactory
@ -24,44 +24,14 @@ class DocumentFactory
}
/**
* @param class-string<Invoice> $type
* @param Collection<(int|string), Member> $members
*/
public function singleInvoice(string $type, InvoiceMemberQuery $query): ?Invoice
public function afterSingle(Invoice $invoice, Collection $members): void
{
$pages = $query->getPages($type);
if ($pages->isEmpty()) {
return null;
}
return $this->resolve($type, $pages);
}
/**
* @param class-string<Invoice> $type
*
* @return Collection<int, Invoice>
*/
public function invoiceCollection(string $type, InvoiceMemberQuery $query): Collection
{
return $query
->getPages($type)
->map(fn ($page) => $this->resolve($type, collect([$page])));
}
public function afterSingle(Invoice $invoice): void
{
foreach ($invoice->allPayments() as $payment) {
foreach ($members as $member) {
foreach ($member->payments as $payment) {
$invoice->afterSingle($payment);
}
}
/**
* @param class-string<Invoice> $type
* @param Collection<int, Page> $pages
*/
private function resolve(string $type, Collection $pages): Invoice
{
return new $type($pages);
}
}

View File

@ -2,10 +2,9 @@
namespace App\Invoice;
use App\Member\Member;
use App\Payment\Payment;
use Carbon\Carbon;
use Exception;
use Generator;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Support\Collection;
use Illuminate\Support\Str;
@ -16,11 +15,9 @@ use Zoomyboy\Tex\Template;
abstract class Invoice extends Document
{
abstract public function getSubject(): string;
abstract public function view(): string;
abstract public function afterSingle(Payment $payment): void;
abstract public function linkLabel(): string;
abstract public static function sendAllLabel(): string;
/**
@ -35,37 +32,46 @@ abstract class Invoice extends Document
*/
abstract public static function getDescription(): array;
abstract public function afterSingle(Payment $payment): void;
/**
* @var Collection<int, Page>
*/
public Collection $pages;
public string $subject;
protected string $filename;
public string $until;
public InvoiceSettings $settings;
public string $filename;
/**
* @param Collection<int, Page> $pages
* @param array<string, string> $positions
*/
public function __construct(Collection $pages)
{
$this->pages = $pages;
$this->subject = $this->getSubject();
public function __construct(
public string $familyName,
public string $singleName,
public string $address,
public string $zip,
public string $location,
public array $positions,
public string $usage,
public ?string $email,
) {
$this->until = now()->addWeeks(2)->format('d.m.Y');
$this->setFilename(Str::slug("{$this->getSubject()} für {$pages->first()?->familyName}"));
$this->settings = app(InvoiceSettings::class);
$this->filename = Str::slug("{$this->getSubject()} für {$familyName}");
}
public function number(int $number): string
/**
* @param Collection<(int|string), Member> $members
*/
public static function fromMembers(Collection $members): self
{
return number_format($number / 100, 2, '.', '');
return static::withoutMagicalCreationFrom([
'familyName' => $members->first()->lastname,
'singleName' => $members->first()->lastname,
'address' => $members->first()->address,
'zip' => $members->first()->zip,
'location' => $members->first()->location,
'email' => $members->first()->email_parents ?: $members->first()->email,
'positions' => static::renderPositions($members),
'usage' => "Mitgliedsbeitrag für {$members->first()->lastname}",
]);
}
public function getUntil(): Carbon
public function settings(): InvoiceSettings
{
return now()->addWeeks(2);
return app(InvoiceSettings::class);
}
public function getEngine(): Engine
@ -92,20 +98,9 @@ abstract class Invoice extends Document
public function getRecipient(): MailRecipient
{
if (!$this->pages->first()?->email) {
throw new Exception('Cannot get Recipient. Mail not set.');
}
throw_unless($this->email, Exception::class, 'Cannot get Recipient. Mail not set.');
return new MailRecipient($this->pages->first()->email, $this->pages->first()->familyName);
}
public function allPayments(): Generator
{
foreach ($this->pages as $page) {
foreach ($page->getPayments() as $payment) {
yield $payment;
}
}
return new MailRecipient($this->email, $this->familyName);
}
/**
@ -113,12 +108,38 @@ abstract class Invoice extends Document
*/
public function mailView(): string
{
$view = 'mail.payment.'.Str::snake(class_basename($this));
$view = 'mail.payment.' . Str::snake(class_basename($this));
if (!view()->exists($view)) {
throw new Exception('Mail view '.$view.' existiert nicht.');
}
throw_unless(view()->exists($view), Exception::class, 'Mail view ' . $view . ' existiert nicht.');
return $view;
}
/**
* @param Collection<int|string, Member> $members
*
* @return array<string, string>
*/
public static function renderPositions(Collection $members): array
{
/** @var array<string, string> */
$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
{
return number_format($number / 100, 2, '.', '');
}
}

View File

@ -1,76 +0,0 @@
<?php
namespace App\Invoice;
use App\Member\Member;
use App\Payment\Payment;
use Illuminate\Support\Collection;
class Page
{
/**
* @var Collection<int, Member>
*/
private Collection $members;
public string $familyName;
public string $singleName;
public string $address;
public string $zip;
public string $location;
public string $usage;
public ?string $email;
/**
* @var array<string, string>
*/
public array $positions;
/**
* @param Collection<int, Member> $members
*/
public function __construct(Collection $members)
{
$this->members = $members;
$this->familyName = $this->members->first()->lastname;
$this->singleName = $members->first()->lastname;
$this->address = $members->first()->address;
$this->zip = $members->first()->zip;
$this->location = $members->first()->location;
$this->email = $members->first()->email_parents ?: $members->first()->email;
$this->positions = $this->getPositions();
$this->usage = "Mitgliedsbeitrag für {$this->familyName}";
}
/**
* @return array<string, string>
*/
public function getPositions(): array
{
/** @var array<string, string> */
$result = [];
foreach ($this->getPayments() 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}"] = $this->number($child->amount);
}
} else {
$result["{$payment->subscription->name} {$payment->nr} für {$payment->member->firstname} {$payment->member->lastname}"] = $this->number($payment->subscription->getAmount());
}
}
return $result;
}
/**
* @return Collection<int, Payment>
*/
public function getPayments(): Collection
{
return $this->members->pluck('payments')->flatten(1);
}
public function number(int $number): string
{
return number_format($number / 100, 2, '.', '');
}
}

View File

@ -3,7 +3,6 @@
namespace App\Invoice\Queries;
use App\Invoice\Invoice;
use App\Invoice\Page;
use App\Member\Member;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Collection as EloquentCollection;
@ -12,35 +11,46 @@ use Illuminate\Support\Str;
abstract class InvoiceMemberQuery
{
/**
* @param class-string<Invoice> $type
*/
public string $type;
/**
* @return Builder<Member>
*/
abstract protected function getQuery(): Builder;
/**
* @param class-string<Invoice> $type
*
* @return Collection<int, Page>
* @return Collection<(int|string), EloquentCollection<(int|string), Member>>
*/
public function getPages(string $type): Collection
public function getMembers(): Collection
{
return $this->get($type)->groupBy(
return $this->get()->groupBy(
fn ($member) => Str::slug(
"{$member->lastname}{$member->address}{$member->zip}{$member->location}",
),
)->map(fn ($page) => new Page($page));
)->toBase();
}
/**
* @param class-string<Invoice> $type
*
*/
public function type(string $type): self
{
$this->type = $type;
return $this;
}
/**
* @return EloquentCollection<int, Member>
*/
private function get(string $type): EloquentCollection
private function get(): EloquentCollection
{
return $this->getQuery()
->with([
'payments' => fn ($query) => $type::paymentsQuery($query)
'payments' => fn ($query) => $this->type::paymentsQuery($query)
->orderByRaw('nr, member_id'),
])
->get()

View File

@ -1,22 +0,0 @@
<?php
namespace App\Invoice\Queries;
use App\Member\Member;
use Illuminate\Database\Eloquent\Builder;
class SingleMemberQuery extends InvoiceMemberQuery
{
public function __construct(
private Member $member
) {
}
/**
* @return Builder<Member>
*/
protected function getQuery(): Builder
{
return Member::where($this->member->only(['lastname', 'address', 'zip', 'location']));
}
}

View File

@ -12,7 +12,18 @@ class Payment extends Model
{
use HasFactory;
public $fillable = ['member_id', 'subscription_id', 'nr', 'status_id', 'last_remembered_at'];
/**
* @var array<int, string>
*/
public $fillable = ['member_id', 'invoice_data', 'subscription_id', 'nr', 'status_id', 'last_remembered_at'];
/**
* @var array<string, string>
*/
public $casts = [
'invoice_data' => 'json',
'last_remembered_at' => 'date',
];
/**
* @return BelongsTo<Member, self>

View File

@ -25,7 +25,7 @@ class PaymentMail extends Mailable
{
$this->invoice = $invoice;
$this->filename = $filename;
$this->salutation = 'Liebe Familie ' . $invoice->pages->first()->familyName;
$this->salutation = 'Liebe Familie ' . $invoice->familyName;
}
/**

View File

@ -3,6 +3,7 @@
namespace App\Payment;
use App\Http\Controllers\Controller;
use App\Invoice\BillDocument;
use App\Invoice\BillKind;
use App\Invoice\DocumentFactory;
use App\Invoice\Queries\BillKindQuery;
@ -30,15 +31,19 @@ class SendpaymentController extends Controller
*/
public function send(Request $request)
{
$invoice = app(DocumentFactory::class)->singleInvoice($request->type, new BillKindQuery(BillKind::POST));
$memberCollection = (new BillKindQuery(BillKind::POST))->type($request->type)->getMembers();
if (is_null($invoice)) {
if ($memberCollection->isEmpty()) {
return response()->noContent();
}
$pdfFile = Tex::compile($invoice);
app(DocumentFactory::class)->afterSingle($invoice);
$documents = $memberCollection->map(function ($members) use ($request) {
$document = $request->type::fromMembers($members);
app(DocumentFactory::class)->afterSingle($document, $members);
return $document;
});
return $pdfFile;
return Tex::merge($documents->all());
}
}

View File

@ -1,27 +0,0 @@
<?php
namespace App\Pdf;
use App\Http\Controllers\Controller;
use App\Invoice\DocumentFactory;
use App\Invoice\Queries\SingleMemberQuery;
use App\Member\Member;
use Illuminate\Contracts\Support\Responsable;
use Illuminate\Http\Request;
use Illuminate\Http\Response;
use Zoomyboy\Tex\Tex;
class MemberPdfController extends Controller
{
/**
* @return Response|Responsable
*/
public function __invoke(Request $request, Member $member)
{
$invoice = app(DocumentFactory::class)->singleInvoice($request->type, new SingleMemberQuery($member));
return null === $invoice
? response()->noContent()
: Tex::compile($invoice);
}
}

1146
composer.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -7,6 +7,7 @@ use App\Payment\Payment;
use App\Payment\Status;
use App\Payment\Subscription;
use Illuminate\Database\Eloquent\Factories\Factory;
use Illuminate\Support\Carbon;
use Tests\RequestFactories\Child;
/**
@ -22,7 +23,7 @@ class PaymentFactory extends Factory
'nr' => $this->faker->year,
'subscription_id' => Subscription::factory()->create()->id,
'status_id' => Status::factory()->create()->id,
'last_remembered_at' => $this->faker->dateTime,
'last_remembered_at' => now(),
];
}
@ -31,6 +32,11 @@ class PaymentFactory extends Factory
return $this->for(Status::whereName('Nicht bezahlt')->first());
}
public function pending(): self
{
return $this->for(Status::whereName('Rechnung gestellt')->first())->state(['last_remembered_at' => now()->subYears(2)]);;
}
public function paid(): self
{
return $this->for(Status::whereName('Rechnung beglichen')->first());

View File

@ -0,0 +1,32 @@
<?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::table('payments', function (Blueprint $table) {
$table->json('invoice_data')->nullable();
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::table('payments', function (Blueprint $table) {
$table->dropColumn('invoice_data');
});
}
};

@ -1 +1 @@
Subproject commit c5ea29af1bb1591238bb037da93739f5bd874334
Subproject commit af626f5d3a14a365e97dc6437025a0b1da6b42bc

@ -1 +1 @@
Subproject commit 6f162102ef7ceca41822d18c3e694abd926f550b
Subproject commit bbab104f7e00c059ffffa115f6b769e2333e137a

View File

@ -304,91 +304,6 @@ parameters:
count: 1
path: packages/laravel-nami/src/Gender.php
-
message: "#^Method Zoomyboy\\\\LaravelNami\\\\Logger\\:\\:__construct\\(\\) has parameter \\$options with no type specified\\.$#"
count: 1
path: packages/laravel-nami/src/Logger.php
-
message: "#^Method Zoomyboy\\\\LaravelNami\\\\Logger\\:\\:__construct\\(\\) has parameter \\$response with no type specified\\.$#"
count: 1
path: packages/laravel-nami/src/Logger.php
-
message: "#^Method Zoomyboy\\\\LaravelNami\\\\Logger\\:\\:__construct\\(\\) has parameter \\$title with no type specified\\.$#"
count: 1
path: packages/laravel-nami/src/Logger.php
-
message: "#^Method Zoomyboy\\\\LaravelNami\\\\Logger\\:\\:__construct\\(\\) has parameter \\$url with no type specified\\.$#"
count: 1
path: packages/laravel-nami/src/Logger.php
-
message: "#^Method Zoomyboy\\\\LaravelNami\\\\Logger\\:\\:fromHttp\\(\\) has no return type specified\\.$#"
count: 1
path: packages/laravel-nami/src/Logger.php
-
message: "#^Method Zoomyboy\\\\LaravelNami\\\\Logger\\:\\:http\\(\\) has no return type specified\\.$#"
count: 1
path: packages/laravel-nami/src/Logger.php
-
message: "#^Method Zoomyboy\\\\LaravelNami\\\\Logger\\:\\:http\\(\\) has parameter \\$options with no type specified\\.$#"
count: 1
path: packages/laravel-nami/src/Logger.php
-
message: "#^Method Zoomyboy\\\\LaravelNami\\\\Logger\\:\\:http\\(\\) has parameter \\$response with no type specified\\.$#"
count: 1
path: packages/laravel-nami/src/Logger.php
-
message: "#^Method Zoomyboy\\\\LaravelNami\\\\Logger\\:\\:http\\(\\) has parameter \\$title with no type specified\\.$#"
count: 1
path: packages/laravel-nami/src/Logger.php
-
message: "#^Method Zoomyboy\\\\LaravelNami\\\\Logger\\:\\:http\\(\\) has parameter \\$url with no type specified\\.$#"
count: 1
path: packages/laravel-nami/src/Logger.php
-
message: "#^Method Zoomyboy\\\\LaravelNami\\\\Logger\\:\\:level\\(\\) has no return type specified\\.$#"
count: 1
path: packages/laravel-nami/src/Logger.php
-
message: "#^Property Zoomyboy\\\\LaravelNami\\\\Logger\\:\\:\\$errors has no type specified\\.$#"
count: 1
path: packages/laravel-nami/src/Logger.php
-
message: "#^Property Zoomyboy\\\\LaravelNami\\\\Logger\\:\\:\\$options has no type specified\\.$#"
count: 1
path: packages/laravel-nami/src/Logger.php
-
message: "#^Property Zoomyboy\\\\LaravelNami\\\\Logger\\:\\:\\$response has no type specified\\.$#"
count: 1
path: packages/laravel-nami/src/Logger.php
-
message: "#^Property Zoomyboy\\\\LaravelNami\\\\Logger\\:\\:\\$title has no type specified\\.$#"
count: 1
path: packages/laravel-nami/src/Logger.php
-
message: "#^Property Zoomyboy\\\\LaravelNami\\\\Logger\\:\\:\\$url has no type specified\\.$#"
count: 1
path: packages/laravel-nami/src/Logger.php
-
message: "#^Unsafe usage of new static\\(\\)\\.$#"
count: 1
path: packages/laravel-nami/src/Logger.php
-
message: "#^Method Zoomyboy\\\\LaravelNami\\\\LoginException\\:\\:setReason\\(\\) has no return type specified\\.$#"
count: 1

View File

@ -1,6 +1,6 @@
\documentclass[silvaletter,12pt]{scrlttr2}
\setkomavar{subject}{<<< $subject >>>}
\setkomavar{subject}{<<< $getSubject >>>}
\setkomavar{fromname}[<<<$settings->from>>>]{<<<$settings->from_long>>>}
\setkomavar{frommobilephone}[Mobiltelefon: ]{<<<$settings->mobile>>>}
\setkomavar{fromemail}[E-Mail: ]{<<<$settings->email>>>}
@ -11,17 +11,16 @@
\setkomavar{fromlogo}{\includegraphics[width=2cm]{logo.png}} % stammeslogo
\begin{document}
@foreach($pages as $page)
\begin{letter}{Familie <<< $page->familyName >>>\\<<< $page->address >>>\\<<< $page->zip >>> <<< $page->location >>>}
\begin{letter}{Familie <<< $familyName >>>\\<<< $address >>>\\<<< $zip >>> <<< $location >>>}
\sffamily
\gdef\TotalHT{0}
\opening{Liebe Familie <<< $page->familyName >>>,}
\opening{Liebe Familie <<< $familyName >>>,}
Hiermit stellen wir Ihnen den aktuellen Mitgliedsbeitrag für den \usekomavar*{fromname} und die DPSG in Rechnung. Dieser setzt sich wie folgt zusammen:
\begin{center}
\begin{tabular}{@{}p{0.8\textwidth}|r}
@foreach($page->positions as $desc => $price)
@foreach($positions as $desc => $price)
\product{<<< $desc >>>}{<<< $price >>>}
@endforeach
\hline
@ -35,7 +34,7 @@
Kontoinhaber: & <<<$settings->from_long>>> \\
IBAN: & <<<$settings->iban>>> \\
Bic: & <<<$settings->bic>>> \\
Verwendungszweck: & <<<$page->usage>>>
Verwendungszweck: & <<<$usage>>>
\end{tabular}
Bitte nehmen Sie zur Kenntnis, dass der für jedes Mitglied obligatorische Versicherungsschutz über die DPSG nur dann für Ihr Kind / Ihre Kinder gilt, wenn der Mitgliedsbeitrag bezahlt wurde. Wenn dies nicht geschieht, müssen wir Ihr Kind / Ihre Kinder von allen Pfadfinderaktionen ausschließen. Dazu gehören sowohl die Gruppenstunden sowie Tagesaktionen als auch mehrtägige Lager.
@ -44,6 +43,5 @@
\closing{Viele Grüße \\ Der Stammesvorstand}
\end{letter}
@endforeach
\end{document}

View File

@ -1,6 +1,6 @@
\documentclass[silvaletter,12pt]{scrlttr2}
\setkomavar{subject}{<<< $subject >>>}
\setkomavar{subject}{<<< $getSubject >>>}
\setkomavar{fromname}[<<<$settings->from>>>]{<<<$settings->from_long>>>}
\setkomavar{frommobilephone}[Mobiltelefon: ]{<<<$settings->mobile>>>}
\setkomavar{fromemail}[E-Mail: ]{<<<$settings->email>>>}
@ -11,17 +11,16 @@
\setkomavar{fromlogo}{\includegraphics[width=2cm]{logo.png}} % stammeslogo
\begin{document}
@foreach($pages as $page)
\begin{letter}{Familie <<< $page->familyName >>>\\<<< $page->address >>>\\<<< $page->zip >>> <<< $page->location >>>}
\begin{letter}{Familie <<< $familyName >>>\\<<< $address >>>\\<<< $zip >>> <<< $location >>>}
\sffamily
\gdef\TotalHT{0}
\opening{Liebe Familie <<< $page->familyName >>>,}
\opening{Liebe Familie <<< $familyName >>>,}
Ihr Mitgliedbeitrag ist noch ausstehend. Dieser setzt sich wie folgt zusammen:
\begin{center}
\begin{tabular}{@{}p{0.8\textwidth}|r}
@foreach($page->positions as $desc => $price)
@foreach($positions as $desc => $price)
\product{<<< $desc >>>}{<<< $price >>>}
@endforeach
\hline
@ -35,7 +34,7 @@
Kontoinhaber: & <<<$settings->from_long>>> \\
IBAN: & <<<$settings->iban>>> \\
Bic: & <<<$settings->bic>>> \\
Verwendungszweck: & <<<$page->usage>>>
Verwendungszweck: & <<<$usage>>>
\end{tabular}
Bitte nehmen Sie zur Kenntnis, dass der für jedes Mitglied obligatorische Versicherungsschutz über die DPSG nur dann für Ihr Kind / Ihre Kinder gilt, wenn der Mitgliedsbeitrag bezahlt wurde. Wenn dies nicht geschieht, müssen wir Ihr Kind / Ihre Kinder von allen Pfadfinderaktionen ausschließen. Dazu gehören sowohl die Gruppenstunden sowie Tagesaktionen als auch mehrtägige Lager.
@ -44,6 +43,5 @@
\closing{Viele Grüße \\ Der Stammesvorstand}
\end{letter}
@endforeach
\end{document}

View File

@ -52,7 +52,6 @@ use App\Payment\Actions\PaymentStoreAction;
use App\Payment\Actions\PaymentUpdateAction;
use App\Payment\SendpaymentController;
use App\Payment\SubscriptionController;
use App\Pdf\MemberPdfController;
Route::group(['namespace' => 'App\\Http\\Controllers'], function (): void {
Auth::routes(['register' => false]);
@ -71,8 +70,6 @@ Route::group(['middleware' => 'auth:web'], function (): void {
Route::get('allpayment', AllpaymentPageAction::class)->name('allpayment.page');
Route::post('allpayment', AllpaymentStoreAction::class)->name('allpayment.store');
Route::resource('subscription', SubscriptionController::class);
Route::get('/member/{member}/pdf', MemberPdfController::class)
->name('member.singlepdf');
Route::get('/sendpayment', [SendpaymentController::class, 'create'])->name('sendpayment.create');
Route::get('/sendpayment/pdf', [SendpaymentController::class, 'send'])->name('sendpayment.pdf');
Route::get('/member/{member}/efz', ShowEfzDocumentAction::class)->name('efz');

View File

@ -3,8 +3,11 @@
namespace Tests\Feature\Invoice;
use App\Invoice\BillDocument;
use App\Invoice\BillKind;
use App\Invoice\DocumentFactory;
use App\Invoice\Invoice;
use App\Invoice\InvoiceSettings;
use App\Invoice\Queries\BillKindQuery;
use App\Invoice\Queries\InvoiceMemberQuery;
use App\Invoice\Queries\SingleMemberQuery;
use App\Invoice\RememberDocument;
@ -15,7 +18,7 @@ use Tests\RequestFactories\Child;
use Tests\TestCase;
use Zoomyboy\Tex\Tex;
class DocumentFactoryTest extends TestCase
class BillRememberDocumentTest extends TestCase
{
use DatabaseTransactions;
@ -30,13 +33,14 @@ class DocumentFactoryTest extends TestCase
'zip' => '::zip::',
'location' => '::location::',
])
->postBillKind()
->has(Payment::factory()->notPaid()->nr('1995')->subscription('::subName::', [
new Child('a', 1000),
new Child('a', 500),
]))
->create();
$invoice = app(DocumentFactory::class)->singleInvoice(BillDocument::class, $this->query($member));
$invoice = BillDocument::fromMembers($this->query(BillDocument::class)->getMembers()->first());
$invoice->assertHasAllContent([
'Rechnung',
@ -51,6 +55,7 @@ class DocumentFactoryTest extends TestCase
{
$member = Member::factory()
->defaults()
->postBillKind()
->state([
'firstname' => '::firstname::',
'lastname' => '::lastname::',
@ -61,7 +66,7 @@ class DocumentFactoryTest extends TestCase
], ['split' => true]))
->create();
$invoice = app(DocumentFactory::class)->singleInvoice(BillDocument::class, $this->query($member));
$invoice = BillDocument::fromMembers($this->query(BillDocument::class)->getMembers()->first());
$invoice->assertHasAllContent([
'Rechnung',
@ -75,46 +80,48 @@ class DocumentFactoryTest extends TestCase
public function testBillSetsFilename(): void
{
$member = Member::factory()
Member::factory()
->defaults()
->postBillKind()
->state(['lastname' => '::lastname::'])
->has(Payment::factory()->notPaid()->nr('1995'))
->create();
$invoice = app(DocumentFactory::class)->singleInvoice(BillDocument::class, $this->query($member));
$invoice = BillDocument::fromMembers($this->query(BillDocument::class)->getMembers()->first());
$this->assertEquals('rechnung-fur-lastname.pdf', $invoice->compiledFilename());
}
public function testRememberSetsFilename(): void
{
$member = Member::factory()
Member::factory()
->postBillKind()
->defaults()
->state(['lastname' => '::lastname::'])
->has(Payment::factory()->notPaid()->state(['last_remembered_at' => now()->subMonths(6)]))
->create();
$invoice = app(DocumentFactory::class)->singleInvoice(RememberDocument::class, $this->query($member));
$invoice = RememberDocument::fromMembers($this->query(RememberDocument::class)->getMembers()->first());
$this->assertEquals('zahlungserinnerung-fur-lastname.pdf', $invoice->compiledFilename());
}
public function testItCreatesOneFileForFamilyMembers(): void
{
$firstMember = Member::factory()
Member::factory()
->defaults()
->postBillKind()
->state(['firstname' => 'Max1', 'lastname' => '::lastname::', 'address' => '::address::', 'zip' => '12345', 'location' => '::location::'])
->has(Payment::factory()->notPaid()->nr('nr1'))
->create();
Member::factory()
->defaults()
->postBillKind()
->state(['firstname' => 'Max2', 'lastname' => '::lastname::', 'address' => '::address::', 'zip' => '12345', 'location' => '::location::'])
->has(Payment::factory()->notPaid()->nr('nr2'))
->create();
$invoice = app(DocumentFactory::class)->singleInvoice(BillDocument::class, $this->query($firstMember));
$invoice->assertHasAllContent(['Max1', 'Max2', 'nr1', 'nr2']);
$this->assertCount(2, $this->query(BillDocument::class)->getMembers()->first());
}
/**
@ -135,12 +142,13 @@ class DocumentFactoryTest extends TestCase
'iban' => 'DE444',
'bic' => 'SOLSSSSS',
]);
$member = Member::factory()
Member::factory()
->defaults()
->has(Payment::factory()->notPaid()->nr('nr2'))
->postBillKind()
->has(Payment::factory()->notPaid()->nr('nr2')->state(['last_remembered_at' => now()->subYear()]))
->create();
$invoice = app(DocumentFactory::class)->singleInvoice($type, $this->query($member));
$invoice = BillDocument::fromMembers($this->query(BillDocument::class)->getMembers()->first());
$invoice->assertHasAllContent([
'langer Stammesname',
@ -156,26 +164,11 @@ class DocumentFactoryTest extends TestCase
]);
}
public function testItGeneratesAPdf(): void
/**
* @param class-string<Invoice> $type
*/
private function query(string $type): InvoiceMemberQuery
{
Tex::fake();
$member = Member::factory()
->defaults()
->has(Payment::factory()->notPaid())
->create(['lastname' => 'lastname']);
$this->withoutExceptionHandling();
$this->login()->init()->loginNami();
$response = $this->call('GET', "/member/{$member->id}/pdf", [
'type' => BillDocument::class,
]);
$this->assertEquals('application/pdf', $response->headers->get('content-type'));
$this->assertEquals('inline; filename="rechnung-fur-lastname.pdf"', $response->headers->get('content-disposition'));
}
private function query(Member $member): InvoiceMemberQuery
{
return new SingleMemberQuery($member);
return (new BillKindQuery(BillKind::POST))->type($type);
}
}

View File

@ -8,7 +8,6 @@ use App\Member\Member;
use App\Payment\Payment;
use App\Payment\PaymentMail;
use Illuminate\Foundation\Testing\DatabaseTransactions;
use Illuminate\Support\Facades\Artisan;
use Illuminate\Support\Facades\Mail;
use Illuminate\Support\Facades\Storage;
use Tests\RequestFactories\Child;
@ -19,42 +18,29 @@ class InvoiceSendActionTest extends TestCase
{
use DatabaseTransactions;
public Member $member;
public function setUp(): void
public function testItCanCreatePdfPayments(): void
{
parent::setUp();
Mail::fake();
Tex::spy();
Storage::fake('temp');
$this->withoutExceptionHandling();
$this->login()->loginNami();
$this->member = Member::factory()
$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']);
}
public function testItCanCreatePdfPayments(): void
{
Mail::fake();
Artisan::call('invoice:send');
Mail::assertSent(PaymentMail::class, fn ($mail) => Storage::disk('temp')->path('rechnung-fur-mom.pdf') === $mail->filename && Storage::disk('temp')->exists('rechnung-fur-mom.pdf'));
}
public function testItCanCompileAttachment(): void
{
Mail::fake();
Tex::spy();
InvoiceSendAction::run();
Tex::assertCompiled(BillDocument::class, fn ($document) => 'Mom' === $document->pages->first()->familyName
&& $document->pages->first()->getPositions() === ['tollerbeitrag 1997 für Lah Mom' => '54.00']
Mail::assertSent(PaymentMail::class, fn ($mail) => Storage::disk('temp')->path('rechnung-fur-mom.pdf') === $mail->filename && Storage::disk('temp')->exists('rechnung-fur-mom.pdf'));
Tex::assertCompiled(
BillDocument::class,
fn ($document) => 'Mom' === $document->familyName
&& $document->positions === ['tollerbeitrag 1997 für Lah Mom' => '54.00']
);
Tex::assertCompiledContent(BillDocument::class, BillDocument::from($member->payments->first()->invoice_data)->renderBody());
}
}

View File

@ -3,7 +3,12 @@
namespace Tests\Feature\Sendpayment;
use App\Invoice\BillDocument;
use App\Invoice\BillKind;
use App\Invoice\DocumentFactory;
use App\Invoice\InvoiceSettings;
use App\Invoice\Queries\BillKindQuery;
use App\Invoice\Queries\SingleMemberQuery;
use App\Invoice\RememberDocument;
use App\Member\Member;
use App\Payment\Payment;
use App\Payment\Status;
@ -30,27 +35,81 @@ class SendpaymentTest extends TestCase
$this->assertStringContainsString('BillDocument', $href);
}
public function testItDownloadsPdfOfAllMembersForBill(): void
{
InvoiceSettings::fake(InvoiceSettingsFake::new()->create());
$this->withoutExceptionHandling()->login()->loginNami();
Member::factory()->defaults()->postBillKind()->count(3)
->has(Payment::factory()->notPaid()->subscription('tollerbeitrag', [new Child('a', 5400)]))
->create();
$response = $this->call('GET', route('sendpayment.pdf'), ['type' => 'App\\Invoice\\BillDocument']);
$response->assertOk();
$this->assertPdfPageCount(3, $response->getFile());
}
public function testItDownloadsPdfOfAllMembersForRemember(): void
{
InvoiceSettings::fake(InvoiceSettingsFake::new()->create());
$this->withoutExceptionHandling()->login()->loginNami();
Member::factory()->defaults()->postBillKind()->count(3)
->has(Payment::factory()->pending()->subscription('tollerbeitrag', [new Child('a', 5400)]))
->create();
$response = $this->call('GET', route('sendpayment.pdf'), ['type' => 'App\\Invoice\\RememberDocument']);
$response->assertOk();
$this->assertPdfPageCount(3, $response->getFile());
}
public function testItCanCreatePdfPayments(): void
{
InvoiceSettings::fake(InvoiceSettingsFake::new()->create());
Tex::spy();
$this->withoutExceptionHandling();
$this->login()->loginNami();
$member = Member::factory()
$this->withoutExceptionHandling()->login()->loginNami();
$members = Member::factory()
->defaults()
->has(Payment::factory()->notPaid()->nr('1997')->subscription('tollerbeitrag', [new Child('a', 5400)]))
->has(Payment::factory()->paid()->nr('1998')->subscription('bezahltdesc', [new Child('b', 5800)]))
->postBillKind()
->count(3)
->create();
$member = $members->first();
$response = $this->call('GET', route('sendpayment.pdf'), ['type' => 'App\\Invoice\\BillDocument']);
$response->assertOk();
$this->call('GET', route('sendpayment.pdf'), ['type' => 'App\\Invoice\\BillDocument']);
$this->assertEquals(Status::firstWhere('name', 'Rechnung gestellt')->id, $member->payments->firstWhere('nr', '1997')->status_id);
$this->assertEquals(Status::firstWhere('name', 'Rechnung beglichen')->id, $member->payments->firstWhere('nr', '1998')->status_id);
Tex::assertCompiled(BillDocument::class, fn ($document) => $document->hasAllContent(['1997', 'tollerbeitrag', '54.00'])
Tex::assertCompiled(
BillDocument::class,
fn ($document) => $document->hasAllContent(['1997', 'tollerbeitrag', '54.00'])
&& $document->missesAllContent(['1998', 'bezahltdesc', '58.00'])
);
$member->payments->firstWhere('nr', '1997')->update(['status_id' => Status::firstWhere('name', 'Nicht bezahlt')->id]);
$invoice = BillDocument::fromMembers((new BillKindQuery(BillKind::POST))->type(BillDocument::class)->getMembers()->first());
$this->assertEquals(
BillDocument::from($member->payments->firstWhere('nr', '1997')->invoice_data)->renderBody(),
$invoice->renderBody()
);
}
public function testItCanCreatePdfPaymentsForRemember(): void
{
InvoiceSettings::fake(InvoiceSettingsFake::new()->create());
Tex::spy();
$this->withoutExceptionHandling()->login()->loginNami();
$member = Member::factory()
->defaults()
->has(Payment::factory()->pending()->nr('1997')->subscription('tollerbeitrag', [new Child('a', 5400)]))
->postBillKind()
->create();
$this->call('GET', route('sendpayment.pdf'), ['type' => 'App\\Invoice\\RememberDocument']);
Tex::assertCompiled(
RememberDocument::class,
fn ($document) => $document->hasAllContent(['1997', 'tollerbeitrag', '54.00'])
);
$this->assertNull($member->payments()->first()->invoice_data);
$this->assertEquals(now()->format('Y-m-d'), $member->payments->first()->last_remembered_at->format('Y-m-d'));
}
public function testItDoesntCreatePdfsWhenUserHasEmail(): void
@ -58,7 +117,7 @@ class SendpaymentTest extends TestCase
Tex::spy();
$this->withoutExceptionHandling();
$this->login()->loginNami();
$member = Member::factory()
Member::factory()
->defaults()
->has(Payment::factory()->notPaid()->nr('1997')->subscription('tollerbeitrag', [new Child('u', 5400)]))
->emailBillKind()

View File

@ -11,6 +11,7 @@ use Illuminate\Http\RedirectResponse;
use Illuminate\Support\Facades\Http;
use Illuminate\Testing\TestResponse;
use Phake;
use Symfony\Component\HttpFoundation\File\File;
use Tests\Lib\MakesHttpCalls;
use Tests\Lib\TestsInertia;
use Zoomyboy\LaravelNami\Authentication\Auth;
@ -81,8 +82,8 @@ abstract class TestCase extends BaseTestCase
$sessionErrors = $response->getSession()->get('errors')->getBag('default');
foreach ($errors as $key => $value) {
$this->assertTrue($sessionErrors->has($key), "Cannot find key {$key} in errors '".print_r($sessionErrors, true));
$this->assertEquals($value, $sessionErrors->get($key)[0], "Failed to validate value for session error key {$key}. Actual value: ".print_r($sessionErrors, true));
$this->assertTrue($sessionErrors->has($key), "Cannot find key {$key} in errors '" . print_r($sessionErrors, true));
$this->assertEquals($value, $sessionErrors->get($key)[0], "Failed to validate value for session error key {$key}. Actual value: " . print_r($sessionErrors, true));
}
return $this;
@ -106,4 +107,14 @@ abstract class TestCase extends BaseTestCase
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]);
}
}