From a8560633800ec90145f95652681dffe72a9b18f3 Mon Sep 17 00:00:00 2001
From: Philipp Lang <philipp@aweos.de>
Date: Mon, 7 Nov 2022 16:18:11 +0100
Subject: [PATCH] Fixed tests

---
 app/Console/Kernel.php                        |   2 -
 app/Contribution/ContributionController.php   |  10 +-
 .../{DvData.php => DvDocument.php}            |  47 +++---
 ...{SolingenData.php => SolingenDocument.php} |  80 +++++-----
 app/Http/Views/MemberView.php                 |  33 ----
 app/Letter/Actions/LetterSendAction.php       |  49 ++++++
 app/Letter/BillDocument.php                   |  62 ++++++++
 app/{Bill => Letter}/BillKind.php             |   2 +-
 app/Letter/DocumentFactory.php                | 149 ++++++++++++++++++
 app/Letter/Letter.php                         | 108 +++++++++++++
 .../LetterSettings.php}                       |   4 +-
 app/Letter/MailRecipient.php                  |  15 ++
 app/Letter/Page.php                           |  68 ++++++++
 .../RememberDocument.php}                     |  44 ++----
 app/{Bill => Letter}/SettingIndexAction.php   |   6 +-
 app/{Bill => Letter}/SettingSaveAction.php    |   4 +-
 app/Member/Member.php                         |   2 +-
 app/Member/MemberController.php               |   2 +-
 app/Payment/ActionFactory.php                 |  20 +--
 app/Payment/PaymentController.php             |  24 ---
 app/Payment/PaymentMail.php                   |  14 +-
 app/Payment/PaymentSendCommand.php            |  80 ----------
 app/Payment/SendpaymentController.php         |  18 ++-
 app/Pdf/BillType.php                          | 125 ---------------
 app/Pdf/Data/MemberEfzData.php                |  68 --------
 app/Pdf/EnvType.php                           |   9 --
 app/Pdf/LetterRepository.php                  |  46 ------
 app/Pdf/MemberPdfController.php               |   8 +-
 app/Pdf/PdfGenerator.php                      |  67 --------
 app/Pdf/PdfRepository.php                     |  16 --
 app/Pdf/PdfRepositoryFactory.php              | 110 -------------
 app/Pdf/Repository.php                        |  46 ------
 app/Setting/SettingServiceProvider.php        |   4 +-
 .../{Bill => Letter}/BillKindFactory.php      |   4 +-
 database/factories/Member/MemberFactory.php   |  15 ++
 ...2020_04_12_223230_create_members_table.php |   2 +-
 packages/tex                                  |   2 +-
 ...type.blade.php => bill_document.blade.php} |   2 +-
 ....blade.php => remember_document.blade.php} |   2 +-
 resources/views/tex/bill.tex                  |  14 +-
 resources/views/tex/remember.tex              |  14 +-
 .../teilnahmeliste.pdf                        | Bin
 resources/views/tex/zuschuss-dv.tex           |  18 +--
 resources/views/tex/zuschuss-stadt.tex        |  10 +-
 routes/web.php                                |   2 +-
 tests/Feature/Bill/SettingTest.php            |   6 +-
 tests/Feature/ContributionTest.php            |  44 ++++++
 tests/Feature/Letter/LetterSendActionTest.php |  56 +++++++
 tests/Feature/Member/StoreTest.php            |   2 +-
 tests/Feature/Pdf/GenerateTest.php            |  35 ++--
 tests/Feature/Sendpayment/SendpaymentTest.php |  68 ++++++++
 51 files changed, 811 insertions(+), 827 deletions(-)
 rename app/Contribution/{DvData.php => DvDocument.php} (77%)
 rename app/Contribution/{SolingenData.php => SolingenDocument.php} (67%)
 create mode 100644 app/Letter/Actions/LetterSendAction.php
 create mode 100644 app/Letter/BillDocument.php
 rename app/{Bill => Letter}/BillKind.php (91%)
 create mode 100644 app/Letter/DocumentFactory.php
 create mode 100644 app/Letter/Letter.php
 rename app/{Bill/BillSettings.php => Letter/LetterSettings.php} (91%)
 create mode 100644 app/Letter/MailRecipient.php
 create mode 100644 app/Letter/Page.php
 rename app/{Pdf/RememberType.php => Letter/RememberDocument.php} (77%)
 rename app/{Bill => Letter}/SettingIndexAction.php (84%)
 rename app/{Bill => Letter}/SettingSaveAction.php (93%)
 delete mode 100644 app/Payment/PaymentSendCommand.php
 delete mode 100644 app/Pdf/BillType.php
 delete mode 100644 app/Pdf/Data/MemberEfzData.php
 delete mode 100644 app/Pdf/EnvType.php
 delete mode 100644 app/Pdf/LetterRepository.php
 delete mode 100644 app/Pdf/PdfGenerator.php
 delete mode 100644 app/Pdf/PdfRepository.php
 delete mode 100644 app/Pdf/PdfRepositoryFactory.php
 delete mode 100644 app/Pdf/Repository.php
 rename database/factories/{Bill => Letter}/BillKindFactory.php (87%)
 rename resources/views/mail/payment/{bill_type.blade.php => bill_document.blade.php} (80%)
 rename resources/views/mail/payment/{remember_type.blade.php => remember_document.blade.php} (82%)
 rename resources/views/tex/templates/{zuschussdv => contribution}/teilnahmeliste.pdf (100%)
 create mode 100644 tests/Feature/ContributionTest.php
 create mode 100644 tests/Feature/Letter/LetterSendActionTest.php
 create mode 100644 tests/Feature/Sendpayment/SendpaymentTest.php

diff --git a/app/Console/Kernel.php b/app/Console/Kernel.php
index 71120fcf..c7dc3746 100644
--- a/app/Console/Kernel.php
+++ b/app/Console/Kernel.php
@@ -2,7 +2,6 @@
 
 namespace App\Console;
 
-use App\Payment\PaymentSendCommand;
 use Illuminate\Console\Scheduling\Schedule;
 use Illuminate\Foundation\Console\Kernel as ConsoleKernel;
 
@@ -14,7 +13,6 @@ class Kernel extends ConsoleKernel
      * @var array
      */
     protected $commands = [
-        PaymentSendCommand::class,
     ];
 
     /**
diff --git a/app/Contribution/ContributionController.php b/app/Contribution/ContributionController.php
index a4cbff8c..b1eb2da9 100644
--- a/app/Contribution/ContributionController.php
+++ b/app/Contribution/ContributionController.php
@@ -6,10 +6,11 @@ use App\Country;
 use App\Http\Controllers\Controller;
 use App\Member\Member;
 use App\Member\MemberResource;
-use App\Pdf\PdfGenerator;
 use Illuminate\Http\Request;
 use Inertia\Inertia;
 use Inertia\Response;
+use Zoomyboy\Tex\BaseCompiler;
+use Zoomyboy\Tex\Tex;
 
 class ContributionController extends Controller
 {
@@ -25,10 +26,11 @@ class ContributionController extends Controller
         ]);
     }
 
-    public function generate(Request $request): PdfGenerator
+    public function generate(Request $request): BaseCompiler
     {
-        $data = app($request->query('type'));
+        /** @var class-string<SolingenDocument> */
+        $type = $request->query('type');
 
-        return app(PdfGenerator::class)->setRepository($data)->render();
+        return Tex::compile($type::fromRequest($request));
     }
 }
diff --git a/app/Contribution/DvData.php b/app/Contribution/DvDocument.php
similarity index 77%
rename from app/Contribution/DvData.php
rename to app/Contribution/DvDocument.php
index c38f1298..04a2c814 100644
--- a/app/Contribution/DvData.php
+++ b/app/Contribution/DvDocument.php
@@ -4,26 +4,33 @@ namespace App\Contribution;
 
 use App\Country;
 use App\Member\Member;
-use App\Pdf\EnvType;
-use App\Pdf\PdfRepository;
 use Carbon\Carbon;
 use Illuminate\Database\Eloquent\Collection;
 use Illuminate\Http\Request;
-use Spatie\LaravelData\Data;
+use Zoomyboy\Tex\Document;
+use Zoomyboy\Tex\Engine;
+use Zoomyboy\Tex\Template;
 
-class DvData extends Data implements PdfRepository
+class DvDocument extends Document
 {
     public function __construct(
         public string $dateFrom,
         public string $dateUntil,
         public string $zipLocation,
         public ?Country $country,
-        public array $members,
+        public Collection $members,
         public ?string $filename = '',
         public string $type = 'F',
     ) {
     }
 
+    public function dateRange(): string
+    {
+        return Carbon::parse($this->dateFrom)->format('d.m.Y')
+            .' - '
+            .Carbon::parse($this->dateUntil)->format('d.m.Y');
+    }
+
     public static function fromRequest(Request $request): self
     {
         return new self(
@@ -31,13 +38,13 @@ class DvData extends Data implements PdfRepository
             dateUntil: $request->dateUntil,
             zipLocation: $request->zipLocation,
             country: Country::findOrFail($request->country),
-            members: $request->members,
+            members: Member::whereIn('id', $request->members)->orderByRaw('lastname, firstname')->get()->chunk(17),
         );
     }
 
-    public function members(): Collection
+    public function countryName(): string
     {
-        return Member::whereIn('id', $this->members)->orderByRaw('lastname, firstname')->get();
+        return $this->country->name;
     }
 
     public function memberShort(Member $member): string
@@ -69,31 +76,19 @@ class DvData extends Data implements PdfRepository
         return (string) $member->getAge();
     }
 
-    public function countryName(): string
-    {
-        return $this->country->name;
-    }
-
-    public function dateRange(): string
-    {
-        return Carbon::parse($this->dateFrom)->format('d.m.Y')
-            .' - '
-            .Carbon::parse($this->dateUntil)->format('d.m.Y');
-    }
-
-    public function getFilename(): string
+    public function basename(): string
     {
         return 'zuschuesse-dv';
     }
 
-    public function getView(): string
+    public function view(): string
     {
         return 'tex.zuschuss-dv';
     }
 
-    public function getTemplate(): ?string
+    public function template(): Template
     {
-        return 'zuschussdv';
+        return Template::make('tex.templates.contribution');
     }
 
     public function setFilename(string $filename): static
@@ -103,8 +98,8 @@ class DvData extends Data implements PdfRepository
         return $this;
     }
 
-    public function getScript(): EnvType
+    public function getEngine(): Engine
     {
-        return EnvType::PDFLATEX;
+        return Engine::PDFLATEX;
     }
 }
diff --git a/app/Contribution/SolingenData.php b/app/Contribution/SolingenDocument.php
similarity index 67%
rename from app/Contribution/SolingenData.php
rename to app/Contribution/SolingenDocument.php
index e759f66b..53b470a4 100644
--- a/app/Contribution/SolingenData.php
+++ b/app/Contribution/SolingenDocument.php
@@ -3,36 +3,61 @@
 namespace App\Contribution;
 
 use App\Member\Member;
-use App\Pdf\EnvType;
-use App\Pdf\PdfRepository;
 use Carbon\Carbon;
 use Illuminate\Database\Eloquent\Collection;
 use Illuminate\Http\Request;
 use Illuminate\Support\Str;
-use Spatie\LaravelData\Data;
+use Zoomyboy\Tex\Document;
+use Zoomyboy\Tex\Engine;
+use Zoomyboy\Tex\Template;
 
-class SolingenData extends Data implements PdfRepository
+class SolingenDocument extends Document
 {
-    public function __construct(
-        public string $eventName,
+    final private function __construct(
         public string $dateFrom,
         public string $dateUntil,
+        public string $zipLocation,
+        /** @var array<int, int> */
         public array $members,
-        public ?string $filename = '',
+        public string $eventName,
         public string $type = 'F',
     ) {
     }
 
-    public static function fromRequest(Request $request): self
+    public static function fromRequest(Request $request): static
     {
-        return new self(
-            eventName: $request->eventName,
+        return new static(
             dateFrom: $request->dateFrom,
             dateUntil: $request->dateUntil,
+            zipLocation: $request->zipLocation,
             members: $request->members,
+            eventName: $request->eventName,
         );
     }
 
+    /**
+     * @return Collection<Collection<Member>>
+     */
+    public function memberModels(): Collection
+    {
+        return Member::whereIn('id', $this->members)->orderByRaw('lastname, firstname')->get()->chunk(14);
+    }
+
+    public function niceEventFrom(): string
+    {
+        return Carbon::parse($this->dateFrom)->format('d.m.Y');
+    }
+
+    public function niceEventUntil(): string
+    {
+        return Carbon::parse($this->dateUntil)->format('d.m.Y');
+    }
+
+    public function template(): Template
+    {
+        return Template::make('tex.templates.contribution');
+    }
+
     public function checkboxes(): string
     {
         $output = '';
@@ -48,45 +73,18 @@ class SolingenData extends Data implements PdfRepository
         return $firstRow."\n".$secondRow;
     }
 
-    public function members(): Collection
-    {
-        return Member::whereIn('id', $this->members)->orderByRaw('lastname, firstname')->get();
-    }
-
-    public function niceEventFrom(): string
-    {
-        return Carbon::parse($this->dateFrom)->format('d.m.Y');
-    }
-
-    public function niceEventTo(): string
-    {
-        return Carbon::parse($this->dateUntil)->format('d.m.Y');
-    }
-
-    public function getFilename(): string
+    public function basename(): string
     {
         return 'zuschuesse-solingen-'.Str::slug($this->eventName);
     }
 
-    public function getView(): string
+    public function view(): string
     {
         return 'tex.zuschuss-stadt';
     }
 
-    public function getTemplate(): ?string
+    public function getEngine(): Engine
     {
-        return null;
-    }
-
-    public function setFilename(string $filename): static
-    {
-        $this->filename = $filename;
-
-        return $this;
-    }
-
-    public function getScript(): EnvType
-    {
-        return EnvType::PDFLATEX;
+        return Engine::PDFLATEX;
     }
 }
diff --git a/app/Http/Views/MemberView.php b/app/Http/Views/MemberView.php
index 7bb388f6..052a0381 100644
--- a/app/Http/Views/MemberView.php
+++ b/app/Http/Views/MemberView.php
@@ -6,9 +6,6 @@ use App\Activity;
 use App\Course\Models\Course;
 use App\Member\Member;
 use App\Member\MemberResource;
-use App\Payment\ActionFactory;
-use App\Payment\Payment;
-use App\Payment\PaymentResource;
 use App\Payment\Status;
 use App\Payment\Subscription;
 use App\Region;
@@ -43,34 +40,4 @@ class MemberView
             })->pluck('subactivities', 'id'),
         ];
     }
-
-    public function paymentEdit(Member $member, Payment $payment): MemberResource
-    {
-        return $this->additional($member, [
-            'model' => new PaymentResource($payment),
-            'links' => [['label' => 'Zurück', 'href' => route('member.payment.index', ['member' => $member])]],
-            'mode' => 'edit',
-        ]);
-    }
-
-    public function paymentIndex(Member $member): MemberResource
-    {
-        return $this->additional($member, [
-            'model' => null,
-            'links' => [
-                ['icon' => 'plus', 'href' => route('member.payment.create', ['member' => $member])],
-            ],
-            'payment_links' => app(ActionFactory::class)->forMember($member),
-            'mode' => 'index',
-        ]);
-    }
-
-    private function additional(Member $member, array $overwrites = []): MemberResource
-    {
-        return (new MemberResource($member->load('payments')))
-            ->additional(array_merge([
-                'subscriptions' => Subscription::pluck('name', 'id'),
-                'statuses' => Status::pluck('name', 'id'),
-            ], $overwrites));
-    }
 }
diff --git a/app/Letter/Actions/LetterSendAction.php b/app/Letter/Actions/LetterSendAction.php
new file mode 100644
index 00000000..aaf641b5
--- /dev/null
+++ b/app/Letter/Actions/LetterSendAction.php
@@ -0,0 +1,49 @@
+<?php
+
+namespace App\Letter\Actions;
+
+use App\Letter\DocumentFactory;
+use App\Payment\PaymentMail;
+use Illuminate\Console\Command;
+use Illuminate\Support\Facades\Storage;
+use Lorisleiva\Actions\Concerns\AsAction;
+use Mail;
+use Zoomyboy\Tex\Tex;
+
+class LetterSendAction
+{
+    use AsAction;
+
+    /**
+     * The name and signature of the console command.
+     *
+     * @var string
+     */
+    protected $signature = 'payment:send';
+
+    /**
+     * The console command description.
+     *
+     * @var string
+     */
+    protected $description = 'Sends Bills';
+
+    /**
+     * Execute the console command.
+     */
+    private function handle(): int
+    {
+        foreach (app(DocumentFactory::class)->types as $type) {
+            $letters = app(DocumentFactory::class)->repoCollection($type, 'E-Mail');
+
+            foreach ($letters as $letter) {
+                $letterPath = Storage::path(Tex::compile($letter)->storeIn('/tmp', 'local'));
+                Mail::to($letter->getRecipient())
+                    ->send(new PaymentMail($letter, $letterPath));
+                app(DocumentFactory::class)->afterSingle($letter);
+            }
+        }
+
+        return 0;
+    }
+}
diff --git a/app/Letter/BillDocument.php b/app/Letter/BillDocument.php
new file mode 100644
index 00000000..1a53ebbb
--- /dev/null
+++ b/app/Letter/BillDocument.php
@@ -0,0 +1,62 @@
+<?php
+
+namespace App\Letter;
+
+use App\Payment\Payment;
+use Illuminate\Database\Eloquent\Relations\HasMany;
+
+class BillDocument extends Letter
+{
+    public function linkLabel(): string
+    {
+        return 'Rechnung erstellen';
+    }
+
+    public function getSubject(): string
+    {
+        return 'Rechnung';
+    }
+
+    public function view(): string
+    {
+        return 'tex.bill';
+    }
+
+    public function sendAllLabel(): string
+    {
+        return 'Rechnungen versenden';
+    }
+
+    /**
+     * Get Descriptions for sendpayment page.
+     *
+     * @return array<int, string>
+     */
+    public function getDescription(): array
+    {
+        return [
+            'Diese Funktion erstellt ein PDF mit allen noch nicht versendenden Rechnungen bei den Mitgliedern die Post als Versandweg haben.',
+            'Die Rechnungen werden automatisch auf "Rechnung gestellt" aktualisiert.',
+        ];
+    }
+
+    public function afterSingle(Payment $payment): void
+    {
+        $payment->update(['status_id' => 2]);
+    }
+
+    public function getMailSubject(): string
+    {
+        return 'Jahresrechnung';
+    }
+
+    /**
+     * @param HasMany<Payment> $query
+     *
+     * @return HasMany<Payment>
+     */
+    public static function paymentsQuery(HasMany $query): HasMany
+    {
+        return $query->whereNeedsBill();
+    }
+}
diff --git a/app/Bill/BillKind.php b/app/Letter/BillKind.php
similarity index 91%
rename from app/Bill/BillKind.php
rename to app/Letter/BillKind.php
index daa60022..575b8505 100644
--- a/app/Bill/BillKind.php
+++ b/app/Letter/BillKind.php
@@ -1,6 +1,6 @@
 <?php
 
-namespace App\Bill;
+namespace App\Letter;
 
 use Illuminate\Database\Eloquent\Factories\HasFactory;
 use Illuminate\Database\Eloquent\Model;
diff --git a/app/Letter/DocumentFactory.php b/app/Letter/DocumentFactory.php
new file mode 100644
index 00000000..c37e00ab
--- /dev/null
+++ b/app/Letter/DocumentFactory.php
@@ -0,0 +1,149 @@
+<?php
+
+namespace App\Letter;
+
+use App\Member\Member;
+use Illuminate\Database\Eloquent\Builder;
+use Illuminate\Database\Eloquent\Collection as EloquentCollection;
+use Illuminate\Support\Collection;
+use Illuminate\Support\Str;
+
+class DocumentFactory
+{
+    /**
+     * @var array<int, class-string<Letter>>
+     */
+    public array $types = [
+        BillDocument::class,
+        RememberDocument::class,
+    ];
+
+    /**
+     * @return Collection<int, Letter>
+     */
+    public function getTypes(): Collection
+    {
+        /** @var array<int, Member> */
+        $emptyMembers = [];
+
+        return collect(array_map(fn ($classString) => new $classString(collect($emptyMembers)), $this->types));
+    }
+
+    /**
+     * @param class-string<Letter> $type
+     */
+    public function fromSingleRequest(string $type, Member $member): ?Letter
+    {
+        $members = $this->singleMemberCollection($member, $type);
+
+        if ($members->isEmpty()) {
+            return null;
+        }
+
+        $repo = $this->resolve($type, $members);
+        $repo->setFilename(Str::slug("{$repo->getSubject()} für {$members->first()->singleName}"));
+
+        return $repo;
+    }
+
+    /**
+     * @param class-string<Letter> $type
+     */
+    public function forAll(string $type, string $billKind): ?Letter
+    {
+        $members = $this->toPages($this->allMemberCollection($type, $billKind));
+
+        if ($members->isEmpty()) {
+            return null;
+        }
+
+        return $this->resolve($type, $members)->setFilename('alle-rechnungen');
+    }
+
+    /**
+     * @param class-string<Letter> $type
+     *
+     * @return Collection<int, Letter>
+     */
+    public function repoCollection(string $type, string $billKind): Collection
+    {
+        $pages = $this->toPages($this->allMemberCollection($type, $billKind));
+
+        return $pages->map(fn ($page) => $this->resolve($type, collect([$page])));
+    }
+
+    public function afterSingle(Letter $repo): void
+    {
+        foreach ($repo->allPayments() as $payment) {
+            $repo->afterSingle($payment);
+        }
+    }
+
+    /**
+     * @param class-string<Letter> $type
+     */
+    public function afterAll(string $type, string $billKind): void
+    {
+        $members = $this->allMemberCollection($type, $billKind);
+        $repo = $this->resolve($type, $this->toPages($members));
+
+        $this->afterSingle($repo);
+    }
+
+    /**
+     * @param class-string<Letter> $type
+     *
+     * @return Collection<int, Page>
+     */
+    public function singleMemberCollection(Member $member, string $type): Collection
+    {
+        $members = Member::where($member->only(['lastname', 'address', 'zip', 'location']))
+            ->with([
+                'payments' => fn ($query) => $type::paymentsQuery($query)
+                    ->orderByRaw('nr, member_id'),
+            ])
+            ->get()
+            ->filter(fn (Member $member) => $member->payments->count() > 0);
+
+        return $this->toPages($members);
+    }
+
+    /**
+     * @param class-string<Letter> $type
+     *
+     * @return EloquentCollection<Member>
+     */
+    private function allMemberCollection(string $type, string $billKind): Collection
+    {
+        return Member::whereHas('billKind', fn (Builder $q) => $q->where('name', $billKind))
+            ->with([
+                'payments' => fn ($query) => $type::paymentsQuery($query)
+                    ->orderByRaw('nr, member_id'),
+            ])
+            ->get()
+            ->filter(fn (Member $member) => $member->payments->count() > 0);
+    }
+
+    /**
+     * @param class-string<Letter>  $type
+     * @param Collection<int, Page> $pages
+     */
+    private function resolve(string $type, Collection $pages): Letter
+    {
+        return new $type($pages);
+    }
+
+    /**
+     * @param EloquentCollection<Member> $members
+     *
+     * @return Collection<int, Page>
+     */
+    private function toPages(EloquentCollection $members): Collection
+    {
+        return $members->groupBy(
+            fn ($member) => Str::slug(
+                "{$member->lastname}{$member->address}{$member->zip}{$member->location}",
+            ),
+        )->map(fn ($page) => new Page($page));
+    }
+}
diff --git a/app/Letter/Letter.php b/app/Letter/Letter.php
new file mode 100644
index 00000000..88b6afa0
--- /dev/null
+++ b/app/Letter/Letter.php
@@ -0,0 +1,108 @@
+<?php
+
+namespace App\Letter;
+
+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;
+use Zoomyboy\Tex\Document;
+use Zoomyboy\Tex\Engine;
+use Zoomyboy\Tex\Template;
+
+abstract class Letter extends Document
+{
+    abstract public function getSubject(): string;
+
+    abstract public function view(): string;
+
+    abstract public function linkLabel(): string;
+
+    abstract public function sendAllLabel(): string;
+
+    /**
+     * @param HasMany<Payment> $query
+     *
+     * @return HasMany<Payment>
+     */
+    abstract public static function paymentsQuery(HasMany $query): HasMany;
+
+    /**
+     * @return array<int, string>
+     */
+    abstract public 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;
+
+    /**
+     * @param Collection<int, Page> $pages
+     */
+    public function __construct(Collection $pages)
+    {
+        $this->pages = $pages;
+        $this->subject = $this->getSubject();
+        $this->until = now()->addWeeks(2)->format('d.m.Y');
+        $this->setFilename(Str::slug("{$this->getSubject()} für {$pages->first()?->familyName}"));
+    }
+
+    public function number(int $number): string
+    {
+        return number_format($number / 100, 2, '.', '');
+    }
+
+    public function getUntil(): Carbon
+    {
+        return now()->addWeeks(2);
+    }
+
+    public function getEngine(): Engine
+    {
+        return Engine::XELATEX;
+    }
+
+    public function basename(): string
+    {
+        return $this->filename;
+    }
+
+    public function template(): Template
+    {
+        return Template::make('tex.templates.default');
+    }
+
+    public function setFilename(string $filename): self
+    {
+        $this->filename = $filename;
+
+        return $this;
+    }
+
+    public function getRecipient(): MailRecipient
+    {
+        if (!$this->pages->first()?->email) {
+            throw new Exception('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;
+            }
+        }
+    }
+}
diff --git a/app/Bill/BillSettings.php b/app/Letter/LetterSettings.php
similarity index 91%
rename from app/Bill/BillSettings.php
rename to app/Letter/LetterSettings.php
index 354e4aea..d8aee48d 100644
--- a/app/Bill/BillSettings.php
+++ b/app/Letter/LetterSettings.php
@@ -1,10 +1,10 @@
 <?php
 
-namespace App\Bill;
+namespace App\Letter;
 
 use App\Setting\LocalSettings;
 
-class BillSettings extends LocalSettings
+class LetterSettings extends LocalSettings
 {
     public string $from_long;
 
diff --git a/app/Letter/MailRecipient.php b/app/Letter/MailRecipient.php
new file mode 100644
index 00000000..b59ae145
--- /dev/null
+++ b/app/Letter/MailRecipient.php
@@ -0,0 +1,15 @@
+<?php
+
+namespace App\Letter;
+
+class MailRecipient
+{
+    public string $name;
+    public string $email;
+
+    public function __construct(string $email, string $name)
+    {
+        $this->email = $email;
+        $this->name = $name;
+    }
+}
diff --git a/app/Letter/Page.php b/app/Letter/Page.php
new file mode 100644
index 00000000..520c91d1
--- /dev/null
+++ b/app/Letter/Page.php
@@ -0,0 +1,68 @@
+<?php
+
+namespace App\Letter;
+
+use App\Member\Member;
+use App\Payment\Payment;
+use Illuminate\Database\Eloquent\Collection;
+use Illuminate\Support\Collection as BaseCollection;
+
+class Page
+{
+    /**
+     * @var Collection<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<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
+    {
+        return $this->getPayments()->mapWithKeys(function (Payment $payment) {
+            $key = "Beitrag {$payment->nr} für {$payment->member->firstname} {$payment->member->lastname} ({$payment->subscription->name})";
+
+            return [$key => $this->number($payment->subscription->amount)];
+        })->toArray();
+    }
+
+    /**
+     * @return BaseCollection<int, Payment>
+     */
+    public function getPayments(): BaseCollection
+    {
+        return $this->members->pluck('payments')->flatten(1);
+    }
+
+    public function number(int $number): string
+    {
+        return number_format($number / 100, 2, '.', '');
+    }
+}
diff --git a/app/Pdf/RememberType.php b/app/Letter/RememberDocument.php
similarity index 77%
rename from app/Pdf/RememberType.php
rename to app/Letter/RememberDocument.php
index 4b6c86a5..3d92cfc4 100644
--- a/app/Pdf/RememberType.php
+++ b/app/Letter/RememberDocument.php
@@ -1,20 +1,13 @@
 <?php
 
-namespace App\Pdf;
+namespace App\Letter;
 
-use App\Member\Member;
 use App\Payment\Payment;
+use Illuminate\Database\Eloquent\Relations\HasMany;
 use Illuminate\Support\Collection;
 
-class RememberType extends Repository implements LetterRepository
+class RememberDocument extends Letter
 {
-    public string $filename;
-
-    public function getPayments(Member $member): Collection
-    {
-        return $member->payments()->whereNeedsRemember()->get();
-    }
-
     public function linkLabel(): string
     {
         return 'Erinnerung erstellen';
@@ -37,21 +30,11 @@ class RememberType extends Repository implements LetterRepository
         return $this->filename;
     }
 
-    public function getScript(): EnvType
-    {
-        return EnvType::XELATEX;
-    }
-
-    public function getView(): string
+    public function view(): string
     {
         return 'tex.remember';
     }
 
-    public function getTemplate(): ?string
-    {
-        return 'default';
-    }
-
     public function getPositions(Collection $page): array
     {
         $memberIds = $page->pluck('id')->toArray();
@@ -65,11 +48,6 @@ class RememberType extends Repository implements LetterRepository
         })->toArray();
     }
 
-    public function getFamilyName(Collection $page): string
-    {
-        return $page->first()->lastname;
-    }
-
     public function getAddress(Collection $page): string
     {
         return $page->first()->address;
@@ -92,10 +70,10 @@ class RememberType extends Repository implements LetterRepository
 
     public function getUsage(Collection $page): string
     {
-        return "Mitgliedsbeitrag für {$this->getFamilyName($page)}";
+        return "Mitgliedsbeitrag für {$page->familyName}";
     }
 
-    public function allLabel(): string
+    public function sendAllLabel(): string
     {
         return 'Erinnerungen versenden';
     }
@@ -122,4 +100,14 @@ class RememberType extends Repository implements LetterRepository
     {
         return 'Zahlungserinnerung';
     }
+
+    /**
+     * @param HasMany<Payment> $query
+     *
+     * @return HasMany<Payment>
+     */
+    public static function paymentsQuery(HasMany $query): HasMany
+    {
+        return $query->whereNeedsRemember();
+    }
 }
diff --git a/app/Bill/SettingIndexAction.php b/app/Letter/SettingIndexAction.php
similarity index 84%
rename from app/Bill/SettingIndexAction.php
rename to app/Letter/SettingIndexAction.php
index 22667792..34c0cca1 100644
--- a/app/Bill/SettingIndexAction.php
+++ b/app/Letter/SettingIndexAction.php
@@ -1,6 +1,6 @@
 <?php
 
-namespace App\Bill;
+namespace App\Letter;
 
 use Inertia\Inertia;
 use Inertia\Response;
@@ -13,7 +13,7 @@ class SettingIndexAction
     /**
      * @return array<string, string>
      */
-    public function handle(BillSettings $settings): array
+    public function handle(LetterSettings $settings): array
     {
         return [
             'from_long' => $settings->from_long,
@@ -27,7 +27,7 @@ class SettingIndexAction
         ];
     }
 
-    public function asController(BillSettings $settings): Response
+    public function asController(LetterSettings $settings): Response
     {
         session()->put('menu', 'setting');
         session()->put('title', 'Rechnungs-Einstellungen');
diff --git a/app/Bill/SettingSaveAction.php b/app/Letter/SettingSaveAction.php
similarity index 93%
rename from app/Bill/SettingSaveAction.php
rename to app/Letter/SettingSaveAction.php
index 32a18104..aff1cbd6 100644
--- a/app/Bill/SettingSaveAction.php
+++ b/app/Letter/SettingSaveAction.php
@@ -1,6 +1,6 @@
 <?php
 
-namespace App\Bill;
+namespace App\Letter;
 
 use Illuminate\Http\RedirectResponse;
 use Lorisleiva\Actions\ActionRequest;
@@ -15,7 +15,7 @@ class SettingSaveAction
      */
     public function handle(array $input): void
     {
-        $settings = app(BillSettings::class);
+        $settings = app(LetterSettings::class);
 
         $settings->fill([
             'from_long' => $input['from_long'] ?? '',
diff --git a/app/Member/Member.php b/app/Member/Member.php
index 420e96a5..88af06f3 100644
--- a/app/Member/Member.php
+++ b/app/Member/Member.php
@@ -3,11 +3,11 @@
 namespace App\Member;
 
 use App\Activity;
-use App\Bill\BillKind;
 use App\Confession;
 use App\Country;
 use App\Course\Models\CourseMember;
 use App\Group;
+use App\Letter\BillKind;
 use App\Nationality;
 use App\Payment\Payment;
 use App\Payment\Subscription;
diff --git a/app/Member/MemberController.php b/app/Member/MemberController.php
index ce520185..676f8aef 100644
--- a/app/Member/MemberController.php
+++ b/app/Member/MemberController.php
@@ -3,12 +3,12 @@
 namespace App\Member;
 
 use App\Activity;
-use App\Bill\BillKind;
 use App\Confession;
 use App\Country;
 use App\Gender;
 use App\Http\Controllers\Controller;
 use App\Http\Views\MemberView;
+use App\Letter\BillKind;
 use App\Nationality;
 use App\Payment\Subscription;
 use App\Region;
diff --git a/app/Payment/ActionFactory.php b/app/Payment/ActionFactory.php
index a9b2c4f2..85ecadb7 100644
--- a/app/Payment/ActionFactory.php
+++ b/app/Payment/ActionFactory.php
@@ -2,31 +2,19 @@
 
 namespace App\Payment;
 
-use App\Member\Member;
-use App\Pdf\PdfRepository;
-use App\Pdf\PdfRepositoryFactory;
+use App\Letter\DocumentFactory;
+use App\Letter\Letter;
 use Illuminate\Support\Collection;
 
 class ActionFactory
 {
-    public function forMember(Member $member): Collection
-    {
-        return app(PdfRepositoryFactory::class)->getTypes()->map(function (PdfRepository $repo) use ($member): array {
-            return [
-                'href' => route('member.singlepdf', ['member' => $member, 'type' => get_class($repo)]),
-                'label' => $repo->linkLabel(),
-                'disabled' => !$repo->createable($member),
-            ];
-        });
-    }
-
     public function allLinks(): Collection
     {
-        return app(PdfRepositoryFactory::class)->getTypes()->map(function (PdfRepository $repo) {
+        return app(DocumentFactory::class)->getTypes()->map(function (Letter $repo) {
             return [
                 'link' => [
                     'href' => route('sendpayment.pdf', ['type' => get_class($repo)]),
-                    'label' => $repo->allLabel(),
+                    'label' => $repo->sendAllLabel(),
                 ],
                 'text' => $repo->getDescription(),
             ];
diff --git a/app/Payment/PaymentController.php b/app/Payment/PaymentController.php
index ded1ab5d..600f8866 100644
--- a/app/Payment/PaymentController.php
+++ b/app/Payment/PaymentController.php
@@ -3,25 +3,12 @@
 namespace App\Payment;
 
 use App\Http\Controllers\Controller;
-use App\Http\Views\MemberView;
 use App\Member\Member;
 use Illuminate\Http\RedirectResponse;
 use Illuminate\Http\Request;
-use Inertia\Response;
 
 class PaymentController extends Controller
 {
-    public function index(Request $request, Member $member): Response
-    {
-        session()->put('menu', 'member');
-        session()->put('title', "Zahlungen für Mitglied {$member->fullname}");
-
-        $payload = app(MemberView::class)->index($request, []);
-        $payload['single'] = app(MemberView::class)->paymentIndex($member);
-
-        return \Inertia::render('member/VIndex', $payload);
-    }
-
     public function store(Request $request, Member $member): RedirectResponse
     {
         $member->createPayment($request->validate([
@@ -33,17 +20,6 @@ class PaymentController extends Controller
         return redirect()->back();
     }
 
-    public function edit(Member $member, Request $request, Payment $payment): Response
-    {
-        session()->put('menu', 'member');
-        session()->put('title', "Zahlungen für Mitglied {$member->fullname}");
-
-        $payload = app(MemberView::class)->index($request, []);
-        $payload['single'] = app(MemberView::class)->paymentEdit($member, $payment);
-
-        return \Inertia::render('member/VIndex', $payload);
-    }
-
     public function update(Request $request, Member $member, Payment $payment): RedirectResponse
     {
         $payment->update($request->validate([
diff --git a/app/Payment/PaymentMail.php b/app/Payment/PaymentMail.php
index 910442c3..45a6fefd 100644
--- a/app/Payment/PaymentMail.php
+++ b/app/Payment/PaymentMail.php
@@ -2,7 +2,7 @@
 
 namespace App\Payment;
 
-use App\Pdf\LetterRepository;
+use App\Letter\Letter;
 use Illuminate\Bus\Queueable;
 use Illuminate\Mail\Mailable;
 use Illuminate\Queue\SerializesModels;
@@ -13,18 +13,20 @@ class PaymentMail extends Mailable
     use Queueable;
     use SerializesModels;
 
-    public LetterRepository $repo;
+    public Letter $letter;
     public string $filename;
+    public string $salutation;
 
     /**
      * Create a new message instance.
      *
      * @return void
      */
-    public function __construct(LetterRepository $repo, string $filename)
+    public function __construct(Letter $letter, string $filename)
     {
+        $this->letter = $letter;
         $this->filename = $filename;
-        $this->repo = $repo;
+        $this->salutation = 'Liebe Familie '.$letter->pages->first()->familyName;
     }
 
     /**
@@ -34,11 +36,11 @@ class PaymentMail extends Mailable
      */
     public function build()
     {
-        $template = Str::snake(class_basename($this->repo));
+        $template = Str::snake(class_basename($this->letter));
 
         return $this->markdown('mail.payment.'.$template)
                     ->attach($this->filename)
                     ->replyTo('kasse@stamm-silva.de')
-                    ->subject($this->repo->getMailSubject().' | DPSG Stamm Silva');
+                    ->subject($this->letter->getSubject().' | DPSG Stamm Silva');
     }
 }
diff --git a/app/Payment/PaymentSendCommand.php b/app/Payment/PaymentSendCommand.php
deleted file mode 100644
index 3fbc0e27..00000000
--- a/app/Payment/PaymentSendCommand.php
+++ /dev/null
@@ -1,80 +0,0 @@
-<?php
-
-namespace App\Payment;
-
-use App\Pdf\BillType;
-use App\Pdf\PdfGenerator;
-use App\Pdf\PdfRepositoryFactory;
-use App\Pdf\RememberType;
-use Illuminate\Console\Command;
-use Mail;
-
-class PaymentSendCommand extends Command
-{
-    /**
-     * The name and signature of the console command.
-     *
-     * @var string
-     */
-    protected $signature = 'payment:send';
-
-    /**
-     * The console command description.
-     *
-     * @var string
-     */
-    protected $description = 'Sends Bills';
-
-    /**
-     * Create a new command instance.
-     *
-     * @return void
-     */
-    public function __construct()
-    {
-        parent::__construct();
-    }
-
-    /**
-     * Execute the console command.
-     *
-     * @return int
-     */
-    public function handle()
-    {
-        $this->sendBills();
-        $this->sendRemembers();
-
-        return 0;
-    }
-
-    private function sendBills(): void
-    {
-        $repos = app(PdfRepositoryFactory::class)->repoCollection(BillType::class, 'E-Mail');
-
-        foreach ($repos as $repo) {
-            $generator = app(PdfGenerator::class)->setRepository($repo)->render();
-            $to = (object) [
-                'email' => $repo->getEmail($repo->pages->first()),
-                'name' => $repo->getFamilyName($repo->pages->first()),
-            ];
-            Mail::to($to)->send(new PaymentMail($repo, $generator->getCompiledFilename()));
-            app(PdfRepositoryFactory::class)->afterSingle($repo);
-        }
-    }
-
-    private function sendRemembers(): void
-    {
-        $repos = app(PdfRepositoryFactory::class)->repoCollection(RememberType::class, 'E-Mail');
-
-        foreach ($repos as $repo) {
-            $generator = app(PdfGenerator::class)->setRepository($repo)->render();
-            $to = (object) [
-                'email' => $repo->getEmail($repo->pages->first()),
-                'name' => $repo->getFamilyName($repo->pages->first()),
-            ];
-            Mail::to($to)->send(new PaymentMail($repo, $generator->getCompiledFilename()));
-            app(PdfRepositoryFactory::class)->afterSingle($repo);
-        }
-    }
-}
diff --git a/app/Payment/SendpaymentController.php b/app/Payment/SendpaymentController.php
index 1ef21ffd..a5628fb3 100644
--- a/app/Payment/SendpaymentController.php
+++ b/app/Payment/SendpaymentController.php
@@ -3,13 +3,13 @@
 namespace App\Payment;
 
 use App\Http\Controllers\Controller;
-use App\Pdf\PdfGenerator;
-use App\Pdf\PdfRepositoryFactory;
+use App\Letter\DocumentFactory;
 use Illuminate\Contracts\Support\Responsable;
 use Illuminate\Http\Request;
 use Illuminate\Http\Response;
 use Inertia\Inertia;
 use Inertia\Response as InertiaResponse;
+use Zoomyboy\Tex\Tex;
 
 class SendpaymentController extends Controller
 {
@@ -28,13 +28,15 @@ class SendpaymentController extends Controller
      */
     public function send(Request $request)
     {
-        $repo = app(PdfRepositoryFactory::class)->forAll($request->type, 'Post');
+        $repo = app(DocumentFactory::class)->forAll($request->type, 'Post');
 
-        $pdfFile = app(PdfGenerator::class)->setRepository($repo)->render();
-        app(PdfRepositoryFactory::class)->afterAll($request->type, 'Post');
+        if (is_null($repo)) {
+            return response()->noContent();
+        }
 
-        return null === $repo
-            ? response()->noContent()
-            : $pdfFile;
+        $pdfFile = Tex::compile($repo);
+        app(DocumentFactory::class)->afterAll($request->type, 'Post');
+
+        return $pdfFile;
     }
 }
diff --git a/app/Pdf/BillType.php b/app/Pdf/BillType.php
deleted file mode 100644
index 60a897ca..00000000
--- a/app/Pdf/BillType.php
+++ /dev/null
@@ -1,125 +0,0 @@
-<?php
-
-namespace App\Pdf;
-
-use App\Member\Member;
-use App\Payment\Payment;
-use Illuminate\Support\Collection;
-
-class BillType extends Repository implements LetterRepository
-{
-    public string $filename;
-
-    public function getPayments(Member $member): Collection
-    {
-        return $member->payments()->whereNeedsBill()->get();
-    }
-
-    public function linkLabel(): string
-    {
-        return 'Rechnung erstellen';
-    }
-
-    public function getSubject(): string
-    {
-        return 'Rechnung';
-    }
-
-    public function setFilename(string $filename): static
-    {
-        $this->filename = $filename;
-
-        return $this;
-    }
-
-    public function getScript(): EnvType
-    {
-        return EnvType::XELATEX;
-    }
-
-    public function getFilename(): string
-    {
-        return $this->filename;
-    }
-
-    public function getView(): string
-    {
-        return 'tex.bill';
-    }
-
-    public function getTemplate(): ?string
-    {
-        return 'default';
-    }
-
-    public function getPositions(Collection $page): array
-    {
-        $memberIds = $page->pluck('id')->toArray();
-        $payments = Payment::whereIn('member_id', $memberIds)
-            ->orderByRaw('nr, member_id')->whereNeedsBill()->get();
-
-        return $payments->mapWithKeys(function (Payment $payment) {
-            $key = "Beitrag {$payment->nr} für {$payment->member->firstname} {$payment->member->lastname} ({$payment->subscription->name})";
-
-            return [$key => $this->number($payment->subscription->amount)];
-        })->toArray();
-    }
-
-    public function getFamilyName(Collection $page): string
-    {
-        return $page->first()->lastname;
-    }
-
-    public function getAddress(Collection $page): string
-    {
-        return $page->first()->address;
-    }
-
-    public function getZip(Collection $page): string
-    {
-        return $page->first()->zip;
-    }
-
-    public function getEmail(Collection $page): string
-    {
-        return $page->first()->email_parents ?: $page->first()->email;
-    }
-
-    public function getLocation(Collection $page): string
-    {
-        return $page->first()->location;
-    }
-
-    public function getUsage(Collection $page): string
-    {
-        return "Mitgliedsbeitrag für {$this->getFamilyName($page)}";
-    }
-
-    public function allLabel(): string
-    {
-        return 'Rechnungen versenden';
-    }
-
-    /**
-     * Get Descriptions for sendpayment page.
-     *
-     * @return array<int, string>
-     */
-    public function getDescription(): array
-    {
-        return [
-            'Diese Funktion erstellt ein PDF mit allen noch nicht versendenden Rechnungen bei den Mitgliedern die Post als Versandweg haben.',
-            'Die Rechnungen werden automatisch auf "Rechnung gestellt" aktualisiert.',
-        ];
-    }
-
-    public function afterSingle(Payment $payment): void
-    {
-        $payment->update(['status_id' => 2]);
-    }
-
-    public function getMailSubject(): string
-    {
-        return 'Jahresrechnung';
-    }
-}
diff --git a/app/Pdf/Data/MemberEfzData.php b/app/Pdf/Data/MemberEfzData.php
deleted file mode 100644
index 725c0ac4..00000000
--- a/app/Pdf/Data/MemberEfzData.php
+++ /dev/null
@@ -1,68 +0,0 @@
-<?php
-
-namespace App\Pdf\Data;
-
-use App\Member\Member;
-use App\Pdf\EnvType;
-use App\Pdf\PdfRepository;
-use Illuminate\Http\Request;
-use Illuminate\Support\Str;
-use Spatie\LaravelData\Data;
-
-class MemberEfzData extends Data implements PdfRepository
-{
-    public function __construct(
-        public ?string $name,
-        public ?string $secondLine,
-        public ?string $currentDate,
-        public ?array $sender = [],
-        public ?string $filename = '',
-    ) {
-    }
-
-    public static function fromRequest(Request $request): self
-    {
-        $memberId = $request->member;
-
-        $member = Member::findOrFail($memberId);
-
-        return new self(
-            name: $member->fullname,
-            secondLine: "geb. am {$member->birthday->format('d.m.Y')}, wohnhaft in {$member->location}",
-            currentDate: now()->format('d.m.Y'),
-            sender: [
-                $member->fullname,
-                $member->address,
-                $member->zip.' '.$member->location,
-                'Mglnr.: '.$member->nami_id,
-            ]
-        );
-    }
-
-    public function getFilename(): string
-    {
-        return 'efz-fuer-'.Str::slug($this->name);
-    }
-
-    public function getView(): string
-    {
-        return 'tex.efz';
-    }
-
-    public function getTemplate(): ?string
-    {
-        return 'efz';
-    }
-
-    public function setFilename(string $filename): static
-    {
-        $this->filename = $filename;
-
-        return $this;
-    }
-
-    public function getScript(): EnvType
-    {
-        return EnvType::PDFLATEX;
-    }
-}
diff --git a/app/Pdf/EnvType.php b/app/Pdf/EnvType.php
deleted file mode 100644
index 2afb1ee3..00000000
--- a/app/Pdf/EnvType.php
+++ /dev/null
@@ -1,9 +0,0 @@
-<?php
-
-namespace App\Pdf;
-
-enum EnvType: string
-{
-    case XELATEX = 'XELATEX';
-    case PDFLATEX = 'PDFLATEX';
-}
diff --git a/app/Pdf/LetterRepository.php b/app/Pdf/LetterRepository.php
deleted file mode 100644
index af545d64..00000000
--- a/app/Pdf/LetterRepository.php
+++ /dev/null
@@ -1,46 +0,0 @@
-<?php
-
-namespace App\Pdf;
-
-use App\Member\Member;
-use App\Payment\Payment;
-use Carbon\Carbon;
-use Generator;
-use Illuminate\Support\Collection;
-
-interface LetterRepository extends PdfRepository
-{
-    public function getSubject(): string;
-
-    public function getPositions(Collection $page): array;
-
-    public function getFamilyName(Collection $page): string;
-
-    public function getAddress(Collection $page): string;
-
-    public function getZip(Collection $page): string;
-
-    public function getLocation(Collection $page): string;
-
-    public function createable(Member $member): bool;
-
-    public function getPayments(Member $member): Collection;
-
-    public function linkLabel(): string;
-
-    public function getUntil(): Carbon;
-
-    public function getUsage(Collection $page): string;
-
-    public function allLabel(): string;
-
-    public function getEmail(Collection $page): string;
-
-    public function getDescription(): array;
-
-    public function afterSingle(Payment $payment): void;
-
-    public function getMailSubject(): string;
-
-    public function allPayments(): Generator;
-}
diff --git a/app/Pdf/MemberPdfController.php b/app/Pdf/MemberPdfController.php
index 7dae0cd9..9cf9a255 100644
--- a/app/Pdf/MemberPdfController.php
+++ b/app/Pdf/MemberPdfController.php
@@ -3,10 +3,12 @@
 namespace App\Pdf;
 
 use App\Http\Controllers\Controller;
+use App\Letter\DocumentFactory;
 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
 {
@@ -15,10 +17,10 @@ class MemberPdfController extends Controller
      */
     public function __invoke(Request $request, Member $member)
     {
-        $repo = app(PdfRepositoryFactory::class)->fromSingleRequest($request->type, $member);
+        $document = app(DocumentFactory::class)->fromSingleRequest($request->type, $member);
 
-        return null === $repo
+        return null === $document
             ? response()->noContent()
-            : app(PdfGenerator::class)->setRepository($repo)->render();
+            : Tex::compile($document);
     }
 }
diff --git a/app/Pdf/PdfGenerator.php b/app/Pdf/PdfGenerator.php
deleted file mode 100644
index fc0d5164..00000000
--- a/app/Pdf/PdfGenerator.php
+++ /dev/null
@@ -1,67 +0,0 @@
-<?php
-
-namespace App\Pdf;
-
-use Illuminate\Contracts\Support\Responsable;
-use Illuminate\Support\Str;
-use Storage;
-
-class PdfGenerator implements Responsable
-{
-    private ?string $filename = null;
-    private PdfRepository $repo;
-    private string $dir;
-
-    public function setRepository(PdfRepository $repo): self
-    {
-        $this->repo = $repo;
-
-        return $this;
-    }
-
-    public function render(): self
-    {
-        $this->filename = $this->repo->getFilename();
-        $this->dir = Str::random(32);
-
-        Storage::disk('temp')->put($this->dir.'/'.$this->repo->getFilename().'.tex', $this->compileView());
-        Storage::disk('temp')->makeDirectory($this->dir);
-
-        if ($this->repo->getTemplate()) {
-            $this->copyTemplateTo(Storage::disk('temp')->path($this->dir));
-        }
-
-        $command = 'cd '.Storage::disk('temp')->path($this->dir);
-        $command .= ' && '.env($this->repo->getScript()->value).' --halt-on-error '.$this->repo->getFilename().'.tex';
-        $command .= ' && '.env($this->repo->getScript()->value).' --halt-on-error '.$this->repo->getFilename().'.tex';
-        exec($command, $output, $returnVar);
-
-        return $this;
-    }
-
-    public function compileView(): string
-    {
-        return view()->make($this->repo->getView(), [
-            'data' => $this->repo,
-        ])->render();
-    }
-
-    public function toResponse($request)
-    {
-        return response()->file($this->getCompiledFilename(), [
-            'Content-Type' => 'application/pdf',
-            'Content-Disposition' => "inline; filename=\"{$this->filename}.pdf\"",
-        ]);
-    }
-
-    public function getCompiledFilename(): string
-    {
-        return Storage::disk('temp')->path($this->dir.'/'.$this->filename.'.pdf');
-    }
-
-    private function copyTemplateTo(string $destination): void
-    {
-        $templatePath = resource_path("views/tex/templates/{$this->repo->getTemplate()}");
-        exec('cp '.$templatePath.'/* '.$destination);
-    }
-}
diff --git a/app/Pdf/PdfRepository.php b/app/Pdf/PdfRepository.php
deleted file mode 100644
index f392ebcb..00000000
--- a/app/Pdf/PdfRepository.php
+++ /dev/null
@@ -1,16 +0,0 @@
-<?php
-
-namespace App\Pdf;
-
-interface PdfRepository
-{
-    public function setFilename(string $filename): static;
-
-    public function getFilename(): string;
-
-    public function getView(): string;
-
-    public function getTemplate(): ?string;
-
-    public function getScript(): EnvType;
-}
diff --git a/app/Pdf/PdfRepositoryFactory.php b/app/Pdf/PdfRepositoryFactory.php
deleted file mode 100644
index 0067700c..00000000
--- a/app/Pdf/PdfRepositoryFactory.php
+++ /dev/null
@@ -1,110 +0,0 @@
-<?php
-
-namespace App\Pdf;
-
-use App\Member\Member;
-use Illuminate\Database\Eloquent\Builder;
-use Illuminate\Support\Collection;
-use Illuminate\Support\Str;
-
-class PdfRepositoryFactory
-{
-    /**
-     * @var array<int, class-string<PdfRepository>>
-     */
-    private array $types = [
-        BillType::class,
-        RememberType::class,
-    ];
-
-    /**
-     * @return Collection<int, PdfRepository>
-     */
-    public function getTypes(): Collection
-    {
-        return collect(array_map(fn ($classString) => new $classString(collect()), $this->types));
-    }
-
-    public function fromSingleRequest(string $type, Member $member): ?LetterRepository
-    {
-        $members = $this->singleMemberCollection($member, $type);
-
-        if ($members->isEmpty()) {
-            return null;
-        }
-
-        $repo = $this->resolve($type, $members);
-        $firstMember = $members->first()->first();
-
-        return $repo->setFilename(
-            Str::slug("{$repo->getSubject()} für {$firstMember->lastname}"),
-        );
-    }
-
-    public function forAll(string $type, string $billKind): ?PdfRepository
-    {
-        $members = $this->toMemberGroup($this->allMemberCollection($type, $billKind));
-
-        if ($members->isEmpty()) {
-            return null;
-        }
-
-        return $this->resolve($type, $members)->setFilename('alle-rechnungen');
-    }
-
-    public function repoCollection(string $type, string $billKind): Collection
-    {
-        $members = $this->toMemberGroup($this->allMemberCollection($type, $billKind));
-
-        return $members->map(function (Collection $members) use ($type) {
-            $repo = $this->resolve($type, collect([$members]));
-
-            return $repo->setFilename(Str::slug("{$repo->getSubject()} für {$members->first()->lastname}"));
-        });
-    }
-
-    public function afterSingle(LetterRepository $repo): void
-    {
-        foreach ($repo->allPayments() as $payment) {
-            $repo->afterSingle($payment);
-        }
-    }
-
-    public function afterAll(string $type, string $billKind): void
-    {
-        $members = $this->allMemberCollection($type, $billKind);
-        $repo = $this->resolve($type, $this->toMemberGroup($members));
-
-        $this->afterSingle($repo);
-    }
-
-    public function singleMemberCollection(Member $member, string $type): Collection
-    {
-        $members = Member::where($member->only(['lastname', 'address', 'zip', 'location']))
-            ->get()
-            ->filter(fn (Member $member) => app($type)->createable($member));
-
-        return $this->toMemberGroup($members);
-    }
-
-    private function allMemberCollection(string $type, string $billKind): Collection
-    {
-        return Member::whereHas('billKind', fn (Builder $q) => $q->where('name', $billKind))
-            ->get()
-            ->filter(fn (Member $member) => app($type)->createable($member));
-    }
-
-    private function resolve(string $kind, Collection $members): LetterRepository
-    {
-        return new $kind($members);
-    }
-
-    private function toMemberGroup(Collection $members): Collection
-    {
-        return $members->groupBy(
-            fn ($member) => Str::slug(
-                "{$member->lastname}{$member->address}{$member->zip}{$member->location}",
-            ),
-        );
-    }
-}
diff --git a/app/Pdf/Repository.php b/app/Pdf/Repository.php
deleted file mode 100644
index 4b603783..00000000
--- a/app/Pdf/Repository.php
+++ /dev/null
@@ -1,46 +0,0 @@
-<?php
-
-namespace App\Pdf;
-
-use App\Member\Member;
-use Carbon\Carbon;
-use Generator;
-use Illuminate\Support\Collection;
-
-abstract class Repository
-{
-    abstract public function getPayments(Member $member): Collection;
-
-    public Collection $pages;
-
-    public function __construct(Collection $pages)
-    {
-        $this->pages = $pages;
-    }
-
-    public function number(int $number): string
-    {
-        return number_format($number / 100, 2, '.', '');
-    }
-
-    public function getUntil(): Carbon
-    {
-        return now()->addWeeks(2);
-    }
-
-    public function createable(Member $member): bool
-    {
-        return 0 !== $this->getPayments($member)->count();
-    }
-
-    public function allPayments(): Generator
-    {
-        foreach ($this->pages as $page) {
-            foreach ($page as $member) {
-                foreach ($this->getPayments($member) as $payment) {
-                    yield $payment;
-                }
-            }
-        }
-    }
-}
diff --git a/app/Setting/SettingServiceProvider.php b/app/Setting/SettingServiceProvider.php
index eda8489b..a8e59cf7 100644
--- a/app/Setting/SettingServiceProvider.php
+++ b/app/Setting/SettingServiceProvider.php
@@ -2,7 +2,7 @@
 
 namespace App\Setting;
 
-use App\Bill\BillSettings;
+use App\Letter\LetterSettings;
 use App\Mailman\MailmanSettings;
 use Illuminate\Support\ServiceProvider;
 
@@ -25,7 +25,7 @@ class SettingServiceProvider extends ServiceProvider
      */
     public function boot()
     {
-        app(SettingFactory::class)->register(BillSettings::class);
+        app(SettingFactory::class)->register(LetterSettings::class);
         app(SettingFactory::class)->register(MailmanSettings::class);
     }
 }
diff --git a/database/factories/Bill/BillKindFactory.php b/database/factories/Letter/BillKindFactory.php
similarity index 87%
rename from database/factories/Bill/BillKindFactory.php
rename to database/factories/Letter/BillKindFactory.php
index 1adfbe1b..853bc478 100644
--- a/database/factories/Bill/BillKindFactory.php
+++ b/database/factories/Letter/BillKindFactory.php
@@ -1,8 +1,8 @@
 <?php
 
-namespace Database\Factories\Bill;
+namespace Database\Factories\Letter;
 
-use App\Bill\BillKind;
+use App\Letter\BillKind;
 use Illuminate\Database\Eloquent\Factories\Factory;
 
 /**
diff --git a/database/factories/Member/MemberFactory.php b/database/factories/Member/MemberFactory.php
index 40892479..b3c685e2 100644
--- a/database/factories/Member/MemberFactory.php
+++ b/database/factories/Member/MemberFactory.php
@@ -5,6 +5,7 @@ namespace Database\Factories\Member;
 use App\Country;
 use App\Fee;
 use App\Group;
+use App\Letter\BillKind;
 use App\Member\Member;
 use App\Nationality;
 use App\Payment\Payment;
@@ -60,6 +61,20 @@ class MemberFactory extends Factory
             ->for($subscription);
     }
 
+    public function postBillKind(): self
+    {
+        return $this->state([
+            'bill_kind_id' => BillKind::firstWhere('name', 'Post')->id,
+        ]);
+    }
+
+    public function emailBillKind(): self
+    {
+        return $this->state([
+            'bill_kind_id' => BillKind::firstWhere('name', 'E-Mail')->id,
+        ]);
+    }
+
     public function inNami(int $namiId): self
     {
         return $this->state(['nami_id' => $namiId]);
diff --git a/database/migrations/2020_04_12_223230_create_members_table.php b/database/migrations/2020_04_12_223230_create_members_table.php
index 3cedc6c0..448717f4 100644
--- a/database/migrations/2020_04_12_223230_create_members_table.php
+++ b/database/migrations/2020_04_12_223230_create_members_table.php
@@ -1,6 +1,6 @@
 <?php
 
-use App\Bill\BillKind;
+use App\Letter\BillKind;
 use Illuminate\Database\Migrations\Migration;
 use Illuminate\Database\Schema\Blueprint;
 use Illuminate\Support\Facades\Schema;
diff --git a/packages/tex b/packages/tex
index fb9d7a66..2ffab167 160000
--- a/packages/tex
+++ b/packages/tex
@@ -1 +1 @@
-Subproject commit fb9d7a660c6e170eedf7a11b02f7cfeff14346ab
+Subproject commit 2ffab167ea1628c3b24724c3506b90e5f7120103
diff --git a/resources/views/mail/payment/bill_type.blade.php b/resources/views/mail/payment/bill_document.blade.php
similarity index 80%
rename from resources/views/mail/payment/bill_type.blade.php
rename to resources/views/mail/payment/bill_document.blade.php
index 14b90e51..70d441c7 100644
--- a/resources/views/mail/payment/bill_type.blade.php
+++ b/resources/views/mail/payment/bill_document.blade.php
@@ -1,5 +1,5 @@
 @component('mail::message')
-# Liebe Familie {{ $repo->getFamilyName($repo->pages->first()) }},
+# {{ $salutation }},
 
 Im Anhang findet ihr die aktuelle Rechnung des Stammes Silva für das laufende Jahr. Bitte begleicht diese bis zum angegebenen Datum.
 
diff --git a/resources/views/mail/payment/remember_type.blade.php b/resources/views/mail/payment/remember_document.blade.php
similarity index 82%
rename from resources/views/mail/payment/remember_type.blade.php
rename to resources/views/mail/payment/remember_document.blade.php
index fd14a79a..ff703955 100644
--- a/resources/views/mail/payment/remember_type.blade.php
+++ b/resources/views/mail/payment/remember_document.blade.php
@@ -1,5 +1,5 @@
 @component('mail::message')
-# Liebe Familie {{ $repo->getFamilyName($repo->pages->first()) }},
+# {{ $salutation }},
 
 Hiermit möchten wir euch an die noch ausstehenden Mitgliedsbeiträge des Stammes Silva für das laufende Jahr erinnern. Bitte begleicht diese bis zum angegebenen Datum.
 
diff --git a/resources/views/tex/bill.tex b/resources/views/tex/bill.tex
index 0fe1c01a..78a4e7c2 100644
--- a/resources/views/tex/bill.tex
+++ b/resources/views/tex/bill.tex
@@ -1,19 +1,19 @@
 \documentclass[silvaletter,12pt]{scrlttr2}
 
-\setkomavar{subject}{<<< $data->getSubject() >>>}
+\setkomavar{subject}{<<< $subject >>>}
 
 \begin{document}
-@foreach($data->pages as $page)
-\begin{letter}{Familie <<< $data->getFamilyName($page) >>>\\<<< $data->getAddress($page) >>>\\<<< $data->getZip($page) >>> <<< $data->getLocation($page) >>>}
+@foreach($pages as $page)
+\begin{letter}{Familie <<< $page->familyName >>>\\<<< $page->address >>>\\<<< $page->zip >>> <<< $page->location >>>}
     \sffamily
     \gdef\TotalHT{0}
-    \opening{Liebe Familie <<< $data->getFamilyName($page) >>>,}
+    \opening{Liebe Familie <<< $page->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.5\textwidth}|r}
-            @foreach($data->getPositions($page) as $desc => $price)
+            @foreach($page->positions as $desc => $price)
             \product{<<< $desc >>>}{<<< $price >>>}
             @endforeach
             \hline
@@ -21,13 +21,13 @@
         \end{tabular}
     \end{center}
 
-    Somit bitten wir Sie, den ausstehenden Betrag von \totalttc bis zum \textbf{<<< $data->getUntil()->format('d.m.Y') >>>} auf folgendes Konto zu überweisen:
+    Somit bitten wir Sie, den ausstehenden Betrag von \totalttc bis zum \textbf{<<< $until >>>} auf folgendes Konto zu überweisen:
 
     \begin{tabular}{ll}
         Kontoinhaber: & DPSG Stamm Silva \\
         IBAN: & DE40 3425 0000 0000 2145 51 \\
         Bic: & SOLSDE33XXX \\
-        Verwendungszweck: & <<<$data->getUsage($page)>>>
+        Verwendungszweck: & <<<$page->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.
diff --git a/resources/views/tex/remember.tex b/resources/views/tex/remember.tex
index 4b116d78..1710d9dd 100644
--- a/resources/views/tex/remember.tex
+++ b/resources/views/tex/remember.tex
@@ -1,19 +1,19 @@
 \documentclass[silvaletter,12pt]{scrlttr2}
 
-\setkomavar{subject}{<<< $data->getSubject() >>>}
+\setkomavar{subject}{<<< $subject >>>}
 
 \begin{document}
-@foreach($data->pages as $page)
-\begin{letter}{Familie <<< $data->getFamilyName($page) >>>\\<<< $data->getAddress($page) >>>\\<<< $data->getZip($page) >>> <<< $data->getLocation($page) >>>}
+@foreach($pages as $page)
+\begin{letter}{Familie <<< $page->familyName >>>\\<<< $page->address >>>\\<<< $page->zip >>> <<< $page->location >>>}
     \sffamily
     \gdef\TotalHT{0}
-    \opening{Liebe Familie <<< $data->getFamilyName($page) >>>,}
+    \opening{Liebe Familie <<< $page->familyName >>>,}
 
     Ihr Mitgliedbeitrag ist noch ausstehend. Dieser setzt sich wie folgt zusammen:
 
     \begin{center}
         \begin{tabular}{@{}p{0.5\textwidth}|r}
-            @foreach($data->getPositions($page) as $desc => $price)
+            @foreach($page->positions as $desc => $price)
             \product{<<< $desc >>>}{<<< $price >>>}
             @endforeach
             \hline
@@ -21,13 +21,13 @@
         \end{tabular}
     \end{center}
 
-    Somit bitten wir Sie, den ausstehenden Betrag von \totalttc bis zum \textbf{<<< $data->getUntil()->format('d.m.Y') >>>} auf folgendes Konto zu überweisen:
+    Somit bitten wir Sie, den ausstehenden Betrag von \totalttc bis zum \textbf{<<< $until >>>} auf folgendes Konto zu überweisen:
 
     \begin{tabular}{ll}
         Kontoinhaber: & DPSG Stamm Silva \\
         IBAN: & DE40 3425 0000 0000 2145 51 \\
         Bic: & SOLSDE33XXX \\
-        Verwendungszweck: & <<<$data->getUsage($page)>>>
+        Verwendungszweck: & <<<$page->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.
diff --git a/resources/views/tex/templates/zuschussdv/teilnahmeliste.pdf b/resources/views/tex/templates/contribution/teilnahmeliste.pdf
similarity index 100%
rename from resources/views/tex/templates/zuschussdv/teilnahmeliste.pdf
rename to resources/views/tex/templates/contribution/teilnahmeliste.pdf
diff --git a/resources/views/tex/zuschuss-dv.tex b/resources/views/tex/zuschuss-dv.tex
index 6ed4184f..7e2661ea 100644
--- a/resources/views/tex/zuschuss-dv.tex
+++ b/resources/views/tex/zuschuss-dv.tex
@@ -13,21 +13,21 @@
 \begin{document}
 \noindent \sffamily
 
-@foreach($data->members()->chunk(17) as $chunk)
+@foreach($members as $chunk)
 \begin{tikzpicture}[remember picture,overlay,yscale=-1]
-    \node[anchor=base west] at (38mm,41.62mm) {\bfseries{\large{<<<!!$data->dateRange()!!>>>}}};
-\node[anchor=base west] at (135.2mm,41.62mm) {\bfseries{\large{<<<!!$data->zipLocation!!>>>}}};
-    \node[anchor=base west] at (242.7mm,41.62mm) {\bfseries{\large{<<<!!$data->countryName()!!>>>}}};
+    \node[anchor=base west] at (38mm,41.62mm) {\bfseries{\large{<<<!!$dateRange!!>>>}}};
+\node[anchor=base west] at (135.2mm,41.62mm) {\bfseries{\large{<<<!!$zipLocation!!>>>}}};
+    \node[anchor=base west] at (242.7mm,41.62mm) {\bfseries{\large{<<<!!$countryName!!>>>}}};
 
 \node[thick, cross out,draw=black,text width=2.4mm, text height=2.4mm, inner sep=0mm] at (17.76mm,47.10mm) {};
 
 @foreach($chunk as $i => $member)
     \node[anchor=base, text width=7.75mm, align=center] at ($(16.35mm, 76.6mm + 7mm * <<<$i % 17>>>)$) {<<<$i+1>>>};
-    \node[anchor=base, text width=18mm, align=center] at ($(32.55mm, 76.6mm + 7mm * <<<$i%17>>>)$) {<<<$data->memberShort($member)>>>};
-    \node[anchor=base, text width=70mm, align=center] at ($(80.25mm, 76.6mm + 7mm * <<<$i%17>>>)$) {<<<$data->memberName($member)>>>};
-    \node[anchor=base, text width=118mm, align=center] at ($(178.25mm, 76.6mm + 7mm * <<<$i%17>>>)$) {<<<$data->memberAddress($member)>>>};
-    \node[anchor=base, text width=16mm, align=center] at ($(249.50mm, 76.6mm + 7mm * <<<$i%17>>>)$) {<<<$data->memberGender($member)>>>};
-    \node[anchor=base, text width=16mm, align=center] at ($(269.50mm, 76.6mm + 7mm * <<<$i%17>>>)$) {<<<$data->memberAge($member)>>>};
+    \node[anchor=base, text width=18mm, align=center] at ($(32.55mm, 76.6mm + 7mm * <<<$i%17>>>)$) {<<<$memberShort($member)>>>};
+    \node[anchor=base, text width=70mm, align=center] at ($(80.25mm, 76.6mm + 7mm * <<<$i%17>>>)$) {<<<$memberName($member)>>>};
+    \node[anchor=base, text width=118mm, align=center] at ($(178.25mm, 76.6mm + 7mm * <<<$i%17>>>)$) {<<<$memberAddress($member)>>>};
+    \node[anchor=base, text width=16mm, align=center] at ($(249.50mm, 76.6mm + 7mm * <<<$i%17>>>)$) {<<<$memberGender($member)>>>};
+    \node[anchor=base, text width=16mm, align=center] at ($(269.50mm, 76.6mm + 7mm * <<<$i%17>>>)$) {<<<$memberAge($member)>>>};
 @endforeach
 
 \end{tikzpicture}
diff --git a/resources/views/tex/zuschuss-stadt.tex b/resources/views/tex/zuschuss-stadt.tex
index 4ba9c0b5..7b22a98d 100644
--- a/resources/views/tex/zuschuss-stadt.tex
+++ b/resources/views/tex/zuschuss-stadt.tex
@@ -83,11 +83,11 @@
 \newcommand{\emptycheckbox}{\tikz{\node[text height=0.5cm,text width=0.5cm,inner sep=0cm] at (0,0) {};}}
 
 \begin{document} \sffamily
-@foreach($data->members()->chunk(14) as $chunk)
+@foreach($memberModels as $chunk)
 \begin{tikzpicture}[outer]
     \path (current page.north west) ++(1cm,-1cm) coordinate (OL) -- (current page.north east) ++(-1cm,0cm) coordinate (OR) node[midway,below=0.5cm] {\textbf{TEILNEHMER - / INNENLISTE}};
     \matrix (options) at ($(OL)+(0.5cm,-1cm)$) [matrix of nodes, column sep=0cm,row sep=0.5cm,nodes in empty cells, every node/.style={inner sep=0cm,align=left,text width=6.2cm}, anchor=north west] {
-        <<<!!$data->checkboxes()!!>>>
+        <<<!!$checkboxes!!>>>
     };
     \node[align=left,inner sep=0cm,anchor=west] at (options-2-4.west) {\tikz{\node[draw,very thick,rectangle,text height=0.5cm,text width=0.5cm,inner sep=0cm] (checkbox) at (0,0) {}; \draw[thick] (checkbox.south east) ++(0.2cm,0) -- (checkbox.south east -| options-2-4.south east);}};
 
@@ -95,13 +95,13 @@
     \draw (org.south east -| options-2-2.south west) -- (org.south east -| options-2-4.south east) node[formfill] {DPSG Stamm Silva Solingen Wald};
 
     \node[anchor=north west] (title) at ($(org.south west)+(0cm,-0.5cm)$) {\large{Titel der Maßnahme:}};
-    \draw (title.south east -| options-2-2.south west) -- (title.south east -| options-2-4.south east) node[formfill] {<<<$data->eventName>>>};
+    \draw (title.south east -| options-2-2.south west) -- (title.south east -| options-2-4.south east) node[formfill] {<<<$eventName>>>};
 
     \node[anchor=north west] (datefrom) at ($(title.south west)+(0cm,-0.5cm)$) {\large{Datum vom:}};
-    \draw (datefrom.south east -| options-2-2.south west) -- ($(datefrom.south east -| options-2-2.south east) - (1,0cm)$) node[formfill] {<<<$data->niceEventFrom()>>>};
+    \draw (datefrom.south east -| options-2-2.south west) -- ($(datefrom.south east -| options-2-2.south east) - (1,0cm)$) node[formfill] {<<<$niceEventFrom()>>>};
 
     \node[anchor=south west] (dateuntil) at (options-2-3.south west |- datefrom.south west) {\large{bis:}};
-    \draw[label={east:aaa}] (dateuntil.south east) -- (datefrom.south east -| options-2-3.south east) node[formfill] {<<<$data->niceEventTo()>>>};
+    \draw[label={east:aaa}] (dateuntil.south east) -- (datefrom.south east -| options-2-3.south east) node[formfill] {<<<$niceEventUntil()>>>};
 
     \path[fill=yellow] (datefrom.south -| OL) ++(0,-1.0) rectangle ($(datefrom.south -| OR) + (0,-1.5)$);
 
diff --git a/routes/web.php b/routes/web.php
index 178b8c4e..16416424 100644
--- a/routes/web.php
+++ b/routes/web.php
@@ -25,7 +25,7 @@ Route::group(['middleware' => 'auth:web'], function (): void {
     Route::get('/initialize', InitializeFormAction::class)->name('initialize.form');
     Route::post('/initialize', InitializeAction::class)->name('initialize.store');
     Route::resource('member', MemberController::class);
-    Route::resource('member.payment', PaymentController::class);
+    Route::apiResource('member.payment', PaymentController::class);
     Route::resource('allpayment', AllpaymentController::class);
     Route::resource('subscription', SubscriptionController::class);
     Route::post('/member/{member}/confirm', MemberConfirmController::class);
diff --git a/tests/Feature/Bill/SettingTest.php b/tests/Feature/Bill/SettingTest.php
index 27d9ecf7..0f4ae2fa 100644
--- a/tests/Feature/Bill/SettingTest.php
+++ b/tests/Feature/Bill/SettingTest.php
@@ -2,7 +2,7 @@
 
 namespace Tests\Feature\Bill;
 
-use App\Bill\BillSettings;
+use App\Letter\LetterSettings;
 use Illuminate\Foundation\Testing\DatabaseTransactions;
 use Tests\TestCase;
 
@@ -13,7 +13,7 @@ class SettingTest extends TestCase
     public function testSettingIndex(): void
     {
         $this->withoutExceptionHandling()->login()->loginNami();
-        BillSettings::fake([
+        LetterSettings::fake([
             'from_long' => 'DPSG Stamm Muster',
             'from' => 'Stamm Muster',
             'mobile' => '+49 176 55555',
@@ -74,7 +74,7 @@ class SettingTest extends TestCase
         ]);
 
         $response->assertRedirect('/setting/bill');
-        $settings = app(BillSettings::class);
+        $settings = app(LetterSettings::class);
         $this->assertEquals('DPSG Stamm Muster', $settings->from_long);
     }
 }
diff --git a/tests/Feature/ContributionTest.php b/tests/Feature/ContributionTest.php
new file mode 100644
index 00000000..428ccabb
--- /dev/null
+++ b/tests/Feature/ContributionTest.php
@@ -0,0 +1,44 @@
+<?php
+
+namespace Tests\Feature;
+
+use App\Country;
+use App\Member\Member;
+use Illuminate\Foundation\Testing\DatabaseTransactions;
+use Tests\TestCase;
+use Zoomyboy\Tex\Tex;
+
+class ContributionTest extends TestCase
+{
+    use DatabaseTransactions;
+
+    /**
+     * @testWith ["App\\Contribution\\SolingenDocument", ["Super tolles Lager", "Max Muster", "Jane Muster", "15.06.1991"]]
+     *  ["App\\Contribution\\DvDocument", ["Muster, Max", "Muster, Jane", "15.06.1991", "42777 SG"]]
+     *
+     * @param array<int, string> $bodyChecks
+     */
+    public function testItCompilesContributionDocuments(string $type, array $bodyChecks): void
+    {
+        $this->withoutExceptionHandling();
+        Tex::spy();
+        $this->login()->loginNami();
+        $country = Country::factory()->create();
+        $member1 = Member::factory()->defaults()->create(['firstname' => 'Max', 'lastname' => 'Muster']);
+        $member2 = Member::factory()->defaults()->create(['firstname' => 'Jane', 'lastname' => 'Muster']);
+
+        $response = $this->call('GET', '/contribution/generate', [
+            'country' => $country->id,
+            'dateFrom' => '1991-06-15',
+            'dateUntil' => '1991-06-16',
+            'eventName' => 'Super tolles Lager',
+            'members' => [$member1->id, $member2->id],
+            'type' => $type,
+            'zipLocation' => '42777 SG',
+        ]);
+
+        $response->assertSessionDoesntHaveErrors();
+        $response->assertOk();
+        Tex::assertCompiled($type, fn ($document) => $document->hasAllContent($bodyChecks));
+    }
+}
diff --git a/tests/Feature/Letter/LetterSendActionTest.php b/tests/Feature/Letter/LetterSendActionTest.php
new file mode 100644
index 00000000..1a0f67c1
--- /dev/null
+++ b/tests/Feature/Letter/LetterSendActionTest.php
@@ -0,0 +1,56 @@
+<?php
+
+namespace Tests\Feature\Letter;
+
+use App\Letter\Actions\LetterSendAction;
+use App\Letter\BillDocument;
+use App\Member\Member;
+use App\Payment\Payment;
+use App\Payment\PaymentMail;
+use Illuminate\Foundation\Testing\DatabaseTransactions;
+use Illuminate\Support\Facades\Storage;
+use Mail;
+use Tests\TestCase;
+use Zoomyboy\Tex\Tex;
+
+class LetterSendActionTest extends TestCase
+{
+    use DatabaseTransactions;
+
+    public Member $member;
+
+    public function setUp(): void
+    {
+        parent::setUp();
+
+        Storage::fake('local');
+        $this->withoutExceptionHandling();
+        $this->login()->loginNami();
+        $this->member = Member::factory()
+            ->defaults()
+            ->has(Payment::factory()->notPaid()->nr('1997')->subscription('tollerbeitrag', 5400))
+            ->emailBillKind()
+            ->create(['firstname' => 'Lah', 'lastname' => 'Mom', 'email' => 'peter@example.com']);
+    }
+
+    public function testItCanCreatePdfPayments(): void
+    {
+        Mail::fake();
+
+        LetterSendAction::run();
+
+        Mail::assertSent(PaymentMail::class, fn ($mail) => Storage::path('rechnung-fur-mom.pdf') === $mail->filename);
+    }
+
+    public function testItCanCompileAttachment(): void
+    {
+        Mail::fake();
+        Tex::spy();
+
+        LetterSendAction::run();
+
+        Tex::assertCompiled(BillDocument::class, fn ($document) => 'Mom' === $document->pages->first()->familyName
+            && $document->pages->first()->getPositions() === ['Beitrag 1997 für Lah Mom (tollerbeitrag)' => '54.00']
+        );
+    }
+}
diff --git a/tests/Feature/Member/StoreTest.php b/tests/Feature/Member/StoreTest.php
index ee425ede..80984f69 100644
--- a/tests/Feature/Member/StoreTest.php
+++ b/tests/Feature/Member/StoreTest.php
@@ -3,10 +3,10 @@
 namespace Tests\Feature\Member;
 
 use App\Activity;
-use App\Bill\BillKind;
 use App\Country;
 use App\Fee;
 use App\Gender;
+use App\Letter\BillKind;
 use App\Member\CreateJob;
 use App\Member\Member;
 use App\Nationality;
diff --git a/tests/Feature/Pdf/GenerateTest.php b/tests/Feature/Pdf/GenerateTest.php
index cf3394e8..163b7a22 100644
--- a/tests/Feature/Pdf/GenerateTest.php
+++ b/tests/Feature/Pdf/GenerateTest.php
@@ -5,12 +5,11 @@ namespace Tests\Feature\Pdf;
 use App\Country;
 use App\Fee;
 use App\Group;
+use App\Letter\BillDocument;
+use App\Letter\DocumentFactory;
 use App\Member\Member;
 use App\Nationality;
 use App\Payment\Subscription;
-use App\Pdf\BillType;
-use App\Pdf\PdfGenerator;
-use App\Pdf\PdfRepositoryFactory;
 use Carbon\Carbon;
 use Database\Factories\Member\MemberFactory;
 use Database\Factories\Payment\PaymentFactory;
@@ -38,18 +37,18 @@ class GenerateTest extends TestCase
             'no_pdf_when_no_bill' => [
                 'members' => [
                     [
-                        'factory' => fn (MemberFactory $member): MemberFactory => $member,
+                        'factory' => fn (MemberFactory $member) => $member,
                         'payments' => [],
                     ],
                 ],
                 'urlCallable' => fn (Collection $members): int => $members->first()->id,
-                'type' => BillType::class,
+                'type' => BillDocument::class,
                 'filename' => null,
             ],
             'bill_for_single_member_when_no_bill_received_yet' => [
                 'members' => [
                     [
-                        'factory' => fn (MemberFactory $member): MemberFactory => $member
+                        'factory' => fn (MemberFactory $member) => $member
                             ->state([
                                 'firstname' => '::firstname::',
                                 'lastname' => '::lastname::',
@@ -58,7 +57,7 @@ class GenerateTest extends TestCase
                                 'location' => '::location::',
                             ]),
                         'payments' => [
-                            fn (PaymentFactory $payment): PaymentFactory => $payment
+                            fn (PaymentFactory $payment) => $payment
                                 ->notPaid()
                                 ->nr('1995')
                                 ->subscription('::subName::', 1500),
@@ -66,7 +65,7 @@ class GenerateTest extends TestCase
                     ],
                 ],
                 'urlCallable' => fn (Collection $members): int => $members->first()->id,
-                'type' => BillType::class,
+                'type' => BillDocument::class,
                 'filename' => 'rechnung-fur-lastname.pdf',
                 'output' => [
                     'Rechnung',
@@ -79,19 +78,19 @@ class GenerateTest extends TestCase
             'bill_has_deadline' => [
                 'members' => [
                     [
-                        'factory' => fn (MemberFactory $member): MemberFactory => $member
+                        'factory' => fn (MemberFactory $member) => $member
                             ->state([
                                 'firstname' => '::firstname::',
                                 'lastname' => '::lastname::',
                             ]),
                         'payments' => [
-                            fn (PaymentFactory $payment): PaymentFactory => $payment
+                            fn (PaymentFactory $payment) => $payment
                                 ->nr('A')->notPaid()->subscription('::subName::', 1500),
                         ],
                     ],
                 ],
                 'urlCallable' => fn (Collection $members): int => $members->first()->id,
-                'type' => BillType::class,
+                'type' => BillDocument::class,
                 'filename' => 'rechnung-fur-lastname.pdf',
                 'output' => [
                     '29.04.2021',
@@ -100,7 +99,7 @@ class GenerateTest extends TestCase
             'families' => [
                 'members' => [
                     [
-                        'factory' => fn (MemberFactory $member): MemberFactory => $member
+                        'factory' => fn (MemberFactory $member) => $member
                             ->state([
                                 'firstname' => '::firstname1::',
                                 'lastname' => '::lastname::',
@@ -109,12 +108,12 @@ class GenerateTest extends TestCase
                                 'location' => '::location::',
                             ]),
                         'payments' => [
-                            fn (PaymentFactory $payment): PaymentFactory => $payment
+                            fn (PaymentFactory $payment) => $payment
                                 ->nr('::nr::')->notPaid()->subscription('::subName::', 1500),
                         ],
                     ],
                     [
-                        'factory' => fn (MemberFactory $member): MemberFactory => $member
+                        'factory' => fn (MemberFactory $member) => $member
                             ->state([
                                 'firstname' => '::firstname2::',
                                 'lastname' => '::lastname::',
@@ -123,13 +122,13 @@ class GenerateTest extends TestCase
                                 'location' => '::location::',
                             ]),
                         'payments' => [
-                            fn (PaymentFactory $payment): PaymentFactory => $payment
+                            fn (PaymentFactory $payment) => $payment
                                 ->nr('::nr2::')->notPaid()->subscription('::subName2::', 1600),
                         ],
                     ],
                 ],
                 'urlCallable' => fn (Collection $members): int => $members->first()->id,
-                'type' => BillType::class,
+                'type' => BillDocument::class,
                 'filename' => 'rechnung-fur-lastname.pdf',
                 'output' => [
                     '::nr::',
@@ -155,7 +154,7 @@ class GenerateTest extends TestCase
 
         $urlId = call_user_func($urlCallable, $members);
         $member = Member::find($urlId);
-        $repo = app(PdfRepositoryFactory::class)->fromSingleRequest($type, $member);
+        $repo = app(DocumentFactory::class)->fromSingleRequest($type, $member);
 
         if (null === $filename) {
             $this->assertNull($repo);
@@ -163,7 +162,7 @@ class GenerateTest extends TestCase
             return;
         }
 
-        $content = app(PdfGenerator::class)->setRepository($repo)->compileView();
+        $content = $repo->renderBody();
 
         foreach ($output as $out) {
             $this->assertStringContainsString($out, $content);
diff --git a/tests/Feature/Sendpayment/SendpaymentTest.php b/tests/Feature/Sendpayment/SendpaymentTest.php
new file mode 100644
index 00000000..72586d92
--- /dev/null
+++ b/tests/Feature/Sendpayment/SendpaymentTest.php
@@ -0,0 +1,68 @@
+<?php
+
+namespace Tests\Feature\Sendpayment;
+
+use App\Letter\BillDocument;
+use App\Member\Member;
+use App\Payment\Payment;
+use App\Payment\Status;
+use Illuminate\Foundation\Testing\DatabaseTransactions;
+use Tests\TestCase;
+use Zoomyboy\Tex\Tex;
+
+class SendpaymentTest extends TestCase
+{
+    use DatabaseTransactions;
+
+    public function testItCanViewSendpaymentPage(): void
+    {
+        $this->withoutExceptionHandling();
+        $this->login()->loginNami();
+
+        $response = $this->get(route('sendpayment.create'));
+
+        $response->assertOk();
+        $this->assertInertiaHas('Rechnungen versenden', $response, 'types.0.link.label');
+        $href = $this->inertia($response, 'types.0.link.href');
+        $this->assertStringContainsString('BillDocument', $href);
+    }
+
+    public function testItCanCreatePdfPayments(): void
+    {
+        Tex::spy();
+        $this->withoutExceptionHandling();
+        $this->login()->loginNami();
+        $member = Member::factory()
+            ->defaults()
+            ->has(Payment::factory()->notPaid()->nr('1997')->subscription('tollerbeitrag', 5400))
+            ->has(Payment::factory()->paid()->nr('1998')->subscription('bezahltdesc', 5800))
+            ->postBillKind()
+            ->create();
+
+        $response = $this->call('GET', route('sendpayment.pdf'), ['type' => 'App\\Letter\\BillDocument']);
+
+        $response->assertOk();
+        $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'])
+            && $document->missesAllContent(['1998', 'bezahltdesc', '58.00'])
+        );
+    }
+
+    public function testItDoesntCreatePdfsWhenUserHasEmail(): void
+    {
+        Tex::spy();
+        $this->withoutExceptionHandling();
+        $this->login()->loginNami();
+        $member = Member::factory()
+            ->defaults()
+            ->has(Payment::factory()->notPaid()->nr('1997')->subscription('tollerbeitrag', 5400))
+            ->emailBillKind()
+            ->create();
+
+        $response = $this->call('GET', route('sendpayment.pdf'), ['type' => 'App\\Letter\\BillDocument']);
+
+        $response->assertStatus(204);
+        Tex::assertNotCompiled(BillDocument::class);
+    }
+}