228 lines
8.8 KiB
PHP
228 lines
8.8 KiB
PHP
<?php
|
|
|
|
namespace App\Domains\Event\Actions\CreateIncomeSurplusStatement;
|
|
|
|
use App\Enumerations\ParticipationType;
|
|
use App\Models\CostUnit;
|
|
use App\Models\Event;
|
|
use App\Providers\PdfGenerateAndDownloadProvider;
|
|
use App\Repositories\CostUnitRepository;
|
|
use App\ValueObjects\Amount;
|
|
use Illuminate\Http\Request;
|
|
|
|
/**
|
|
* Erzeugt die Einnahmen-Überschuss-Rechnung einer Veranstaltung als PDF.
|
|
*
|
|
* Gezeigt wird ausschließlich Geld, das geflossen ist: gezahlte Beiträge, weitere Einnahmen, was von
|
|
* Abmeldungen einbehalten wurde, Fördermittel -- und auf der anderen Seite die erfassten Belege. Was nur
|
|
* erwartet (offene Beiträge) oder geplant (Budgetwerte) ist, gehört in eine Einnahmen-Überschuss-Rechnung
|
|
* nicht hinein.
|
|
*
|
|
* Es wird nichts gespeichert: Alle Zahlen leiten sich aus dem aktuellen Stand ab, ein erneuter Abruf
|
|
* liefert den dann gültigen Stand.
|
|
*/
|
|
class CreateIncomeSurplusStatementCommand
|
|
{
|
|
private Event $event;
|
|
|
|
private CostUnitRepository $costUnits;
|
|
|
|
public function __construct(private readonly CreateIncomeSurplusStatementRequest $request)
|
|
{
|
|
$this->event = $request->event;
|
|
$this->costUnits = new CostUnitRepository();
|
|
}
|
|
|
|
public function execute(): CreateIncomeSurplusStatementResponse
|
|
{
|
|
$response = new CreateIncomeSurplusStatementResponse();
|
|
|
|
$costUnit = $this->event->costUnit()->first();
|
|
|
|
if (!$costUnit instanceof CostUnit) {
|
|
$response->message = 'Der Veranstaltung ist keine Kostenstelle zugeordnet.';
|
|
|
|
return $response;
|
|
}
|
|
|
|
// Der Pauschalbetrag wird vor dem Resource-Aufruf gelesen: `EventResource::calculateSupportPerPerson()`
|
|
// multipliziert das Amount-Objekt von `support_per_person` in place. Auf `support_flat` wirkt das
|
|
// zwar nicht, aber der gesamte Zugriff auf Beträge des Models ist danach nicht mehr vertrauenswürdig.
|
|
$otherIncome = $this->event->support_flat->getAmount();
|
|
|
|
$eventData = $this->event->toResource()->toArray(new Request());
|
|
|
|
$income = $this->buildIncome($eventData, $otherIncome);
|
|
$expenses = $this->buildExpenses($costUnit);
|
|
|
|
$result = new Amount($income['total']->getAmount() - $expenses['total']->getAmount(), 'Euro');
|
|
|
|
$html = view('pdfs.income-surplus-statement', [
|
|
'event' => $this->event,
|
|
'createdAt' => new \DateTime()->format('d.m.Y'),
|
|
'income' => $income,
|
|
'expenses' => $expenses,
|
|
'result' => $result,
|
|
'money' => self::money(...),
|
|
])->render();
|
|
|
|
$response->success = true;
|
|
$response->filename = 'EUER-' . $this->event->identifier . '.pdf';
|
|
$response->income = $income;
|
|
$response->expenses = $expenses;
|
|
$response->result = $result;
|
|
$response->pdfContent = PdfGenerateAndDownloadProvider::fromHtml($html, 'portrait');
|
|
|
|
return $response;
|
|
}
|
|
|
|
/**
|
|
* Die Einnahmenseite in zwei Ober-Kategorien.
|
|
*
|
|
* Alle Zahlen stammen aus {@see \App\Resources\EventResource} -- derselben Quelle wie die
|
|
* Veranstaltungsübersicht am Bildschirm. Eine eigene Rechnung daneben würde über kurz oder lang von
|
|
* der Übersicht abweichen, und dann glaubt niemand mehr einer der beiden Zahlen.
|
|
*
|
|
* @param array<string, mixed> $eventData
|
|
*
|
|
* @return array{categories: array<int, array{name: string, total: Amount, entries: array<int, array{name: string, amount: Amount}>}>, total: Amount}
|
|
*/
|
|
private function buildIncome(array $eventData, float $otherIncome): array
|
|
{
|
|
// Beiträge aller Teilnahmearten in einer Zeile: Für die Mittelverwendung zählt, was an Beiträgen
|
|
// hereingekommen ist, nicht von wem.
|
|
$participationFees = new Amount(0, 'Euro');
|
|
foreach ([
|
|
ParticipationType::PARTICIPATION_TYPE_PARTICIPANT,
|
|
ParticipationType::PARTICIPATION_TYPE_TEAM,
|
|
ParticipationType::PARTICIPATION_TYPE_VOLUNTEER,
|
|
ParticipationType::PARTICIPATION_TYPE_OTHER,
|
|
] as $participationType) {
|
|
$participationFees->addAmount(
|
|
new Amount((float) $eventData['participants'][$participationType]['amount']['paid']['value'], 'Euro')
|
|
);
|
|
}
|
|
|
|
$ownFunds = [
|
|
['name' => 'Teilnahmebeiträge', 'amount' => $participationFees],
|
|
['name' => 'Weitere Einnahmen', 'amount' => new Amount($otherIncome, 'Euro')],
|
|
[
|
|
'name' => 'Einbehaltene Einnahmen aus Abmeldungen',
|
|
'amount' => new Amount((float) $eventData['retainedFromUnregistered']['value'], 'Euro'),
|
|
],
|
|
];
|
|
|
|
$supportRate = new Amount((float) $eventData['supportPersonValue'], 'Euro');
|
|
$funding = [
|
|
[
|
|
'name' => 'Fördermittel (' . self::money($supportRate) . ' € p.P./Tag)',
|
|
'amount' => new Amount($eventData['supportPerson']['amount']->getAmount(), 'Euro'),
|
|
],
|
|
];
|
|
|
|
$categories = [
|
|
['name' => 'Eigenmittel', 'entries' => $ownFunds, 'total' => self::sum($ownFunds)],
|
|
['name' => 'Förderungen', 'entries' => $funding, 'total' => self::sum($funding)],
|
|
];
|
|
|
|
return [
|
|
'categories' => $categories,
|
|
'total' => self::sum($categories),
|
|
];
|
|
}
|
|
|
|
/**
|
|
* Die Ausgabenseite: eine Zeile je Ausgabentyp, dazu die Belege für die Anlage.
|
|
*
|
|
* @return array{groups: array<int, array{name: string, sum: Amount, rows: array<int, array{number: string, date: string, purpose: string, amount: Amount}>}>, total: Amount}
|
|
*/
|
|
private function buildExpenses(CostUnit $costUnit): array
|
|
{
|
|
$groups = [];
|
|
$total = new Amount(0, 'Euro');
|
|
|
|
foreach ($this->costUnits->groupExpensesByType($costUnit) as $group) {
|
|
$rows = [];
|
|
|
|
foreach ($group['invoices'] as $invoice) {
|
|
$rows[] = [
|
|
'number' => (string) $invoice->invoice_number,
|
|
'date' => $invoice->created_at?->format('d.m.Y') ?? '',
|
|
'purpose' => $this->purpose($invoice->purposeText(), $invoice->comment),
|
|
'amount' => Amount::fromString($invoice->amount),
|
|
];
|
|
}
|
|
|
|
$groups[] = [
|
|
'name' => $group['type']->name,
|
|
'sum' => $group['sum'],
|
|
'rows' => $rows,
|
|
];
|
|
|
|
$total->addAmount($group['sum']);
|
|
}
|
|
|
|
return ['groups' => $groups, 'total' => $total];
|
|
}
|
|
|
|
/**
|
|
* Wofür der Beleg steht, um die Anmerkung ergänzt.
|
|
*
|
|
* Den Zweck selbst bestimmt {@see \App\Models\Invoice::purposeText()} -- dieselbe Ermittlung wie in
|
|
* der Beleg-Übersicht, damit ein Beleg nicht an zwei Stellen Verschiedenes über sich behauptet. Die
|
|
* Anmerkung kommt nur hier dazu: Auf der Aufstellung steht der Beleg für sich, ohne die Detailansicht
|
|
* daneben.
|
|
*
|
|
* Ältere Belege haben keinen Zweck erfasst -- dann bleibt die Anmerkung, und fehlt auch die, bleibt
|
|
* die Zelle leer. Ein Platzhalter wie "--" würde in der Belegliste nur Platz kosten.
|
|
*/
|
|
private function purpose(?string $purpose, ?string $comment): string
|
|
{
|
|
$parts = [];
|
|
|
|
foreach ([$purpose, $comment] as $part) {
|
|
if (trim((string) $part) !== '') {
|
|
$parts[] = trim((string) $part);
|
|
}
|
|
}
|
|
|
|
return implode(' — ', $parts);
|
|
}
|
|
|
|
/**
|
|
* Ein Betrag in deutscher Schreibweise: Punkt als Tausender-, Komma als Dezimaltrennzeichen.
|
|
*
|
|
* Bewusst nicht {@see Amount::getFormattedAmount()}: Die Methode ersetzt nach `number_format` jeden
|
|
* Punkt durch ein Komma und macht aus 1.487,50 damit "1,487,50". Auf einer Aufstellung, in der
|
|
* vierstellige Beträge die Regel sind, wäre das nicht lesbar. Der Fehler steckt im Value Object und
|
|
* wirkt überall, wo Beträge angezeigt werden -- ihn dort zu beheben ist eine eigene Änderung.
|
|
*
|
|
* Öffentlich, weil die Vorlage sie als Callable bekommt und weil sie für sich prüfbar sein soll.
|
|
*/
|
|
public static function money(Amount $amount): string
|
|
{
|
|
return number_format(round($amount->getAmount(), 2), 2, ',', '.');
|
|
}
|
|
|
|
/**
|
|
* Summiert Zeilen, die je ein `amount` oder `total` tragen.
|
|
*
|
|
* Über ein frisches Amount-Objekt, weil `Amount::addAmount()` den Empfänger verändert -- die
|
|
* Einzelbeträge sollen unangetastet bleiben, sie werden anschließend gedruckt.
|
|
*
|
|
* @param array<int, array<string, mixed>> $rows
|
|
*/
|
|
private static function sum(array $rows): Amount
|
|
{
|
|
$sum = new Amount(0, 'Euro');
|
|
|
|
foreach ($rows as $row) {
|
|
/** @var Amount $amount */
|
|
$amount = $row['amount'] ?? $row['total'];
|
|
$sum->addAmount($amount);
|
|
}
|
|
|
|
return $sum;
|
|
}
|
|
}
|