refund = $request->refund; $this->participant = $request->refund->participant; $this->event = $request->refund->event; // Wie in CreateParticipantInvoiceCommand: `$event->tenant` liefert das Slug-Attribut, nicht die // Relation. Der Aussteller wird live gelesen, damit eine Korrektur an Name oder Anschrift auch // auf bestehende Veranstaltungen wirkt. $this->sender = $this->event->tenant()->first(); } public function execute(): CreateRefundDocumentResponse { $response = new CreateRefundDocumentResponse(); if ($this->event->invoice_key === null || $this->participant->invoice_sequence === null) { $response->message = 'Für diese Anmeldung lässt sich keine Belegnummer bilden.'; return $response; } if (!$this->refund->isAccepted()) { $response->message = 'Der Beleg entsteht erst, wenn die Erstattung bestätigt wurde.'; return $response; } $documentNumber = $this->documentNumber(); $html = new DocumentTemplateRenderProvider(DocumentTemplate::TYPE_PARTICIPANT_REFUND) ->render($this->buildTokens($documentNumber)); $response->success = true; $response->documentNumber = $documentNumber; $response->filename = 'Rueckerstattung-' . $documentNumber . '.pdf'; $response->pdfContent = PdfGenerateAndDownloadProvider::fromHtml($html, 'portrait'); return $response; } /** * Dieselbe Nummer wie die Rechnung, mit angehängtem `-R`. Kein zweiter Nummernkreis: der Beleg * gehört zu genau einer Anmeldung, und so ist auf einen Blick erkennbar, zu welcher Rechnung. */ private function documentNumber(): string { return $this->invoiceNumber() . '-R'; } /** Die Nummer der Teilnahmerechnung -- der Beleg weist sie aus, damit die Zahlung auffindbar ist. */ private function invoiceNumber(): string { return sprintf( '%s-%s', $this->event->invoice_key, str_pad((string) $this->participant->invoice_sequence, 4, '0', STR_PAD_LEFT) ); } /** * Der Erklärungssatz aus `page_texts` -- derselbe, den der Teili auf der Bestätigungsseite gelesen * und angekreuzt hat. * * Mit Rückfallwert: fehlt die Zeile in der Datenbank, soll der Beleg trotzdem entstehen. Ohne den * Fallback stünde hier ein Fatal Error auf `null` -- so steht es heute im Deckblatt-Code der * Auslagenerstattung, und daran soll sich der Beleg kein Beispiel nehmen. */ /** * Der Vermerk, wenn die Aktionsleitung die Angaben aufgenommen hat. * * Er nennt Name und Datum, weil in diesem Fall niemand die Erklärung darüber angekreuzt hat: Wer den * Beleg prüft, soll erkennen, dass dort eine aufgenommene Angabe steht und keine Bestätigung des * Teilis selbst. Beim gewöhnlichen Weg bleibt der Platzhalter leer und der Block fällt weg. */ private function captureNote(): string { if (!$this->refund->wasCapturedByManagement()) { return ''; } $name = $this->refund->capturedBy()->first()?->getOfficialName(); return sprintf( 'Angaben aufgenommen durch %s am %s.', trim((string) $name) !== '' ? $name : 'die Aktionsleitung', $this->refund->accepted_at?->format('d.m.Y') ?? '' ); } private function declarationText(): string { $text = PageText::where('name', self::DECLARATION_TEXT)->first()?->content; return trim((string) $text) !== '' ? (string) $text : 'Ich versichere, dass ich den genannten Betrag beglichen habe und nicht anderweitig ' . 'zurückerstattet bekomme.'; } /** * Leistungszeitraum: bei eintägigen Veranstaltungen nur ein Datum, sonst der Zeitraum. */ private function servicePeriod(): string { $start = $this->event->start_date; $end = $this->event->end_date; if ($start === null) { return ''; } if ($end === null || $start->isSameDay($end)) { return $start->format('d.m.Y'); } return sprintf('%s – %s', $start->format('d.m.Y'), $end->format('d.m.Y')); } /** * @return array */ private function buildTokens(string $documentNumber): array { $participant = $this->participant; $sender = $this->sender; $refund = $this->refund; return [ 'document_title' => 'Rückerstattung ' . $documentNumber, 'document_number' => $documentNumber, // Belegdatum ist der Tag, an dem der Teili bestätigt hat -- da stand der Vorgang fest. 'document_date' => $refund->accepted_at?->format('d.m.Y') ?? '', 'event_name' => (string) $this->event->name, 'service_period' => $this->servicePeriod(), 'unregistered_at' => $participant->unregistered_at?->format('d.m.Y') ?? '', 'sender_name' => $sender?->invoiceSenderName() ?? '', 'sender_address_1' => (string) $sender?->address_1, 'sender_address_2' => (string) $sender?->address_2, 'sender_address_3' => (string) $sender?->address_3, 'sender_postcode' => (string) $sender?->postcode, 'sender_city' => (string) $sender?->city, 'sender_email' => (string) $sender?->email, 'sender_phone' => (string) $sender?->phone, 'sender_tax_number' => (string) $sender?->tax_number, 'sender_vat_id' => (string) $sender?->vat_id, 'recipient_name' => $participant->getOfficialName(), 'recipient_address_1' => (string) $participant->address_1, 'recipient_address_2' => (string) $participant->address_2, 'recipient_postcode' => (string) $participant->postcode, 'recipient_city' => (string) $participant->city, 'paid_amount' => $this->money($participant->amount_paid?->getAmount() ?? 0.0), 'invoice_number' => $this->invoiceNumber(), 'refund_amount' => $this->money($refund->amount?->getAmount() ?? 0.0), 'refund_reason' => $refund->reasonLabel(), 'refund_reason_text' => $refund->reasonText(), 'account_owner' => (string) $refund->account_owner, 'account_iban' => $this->formatIban((string) $refund->account_iban), 'declaration_text' => $this->declarationText(), 'capture_note' => $this->captureNote(), 'details_table' => $this->renderDetails(), ]; } /** * Der generierte Block: wer erklärt, worauf sich die Erstattung bezieht, warum, und auf welches * Konto sie geht. * * Alles, was die Person erklärt, steht in dieser einen Tabelle -- auch die Begründung, die früher als * Fließtext darunter hing. Was daneben steht (Anschrift im Briefkopf, Veranstaltung im Betreff), * beschreibt den Vorgang, gehört aber nicht zur Erklärung selbst. */ private function renderDetails(): string { $refund = $this->refund; $participant = $this->participant; $rows = [ // Der Name steht voran: die Tabelle trägt alles, was die Person erklärt, und die Anschrift // allein im Briefkopf würde den Bezug lösen, sobald der Beleg als Anlage hinter einem // Deckblatt liegt. Kontoinhaber*in weiter unten kann eine andere Person sein -- etwa ein // Elternteil. ['Name', e($participant->getOfficialName())], // Der gezahlte Beitrag ist die Bezugsgröße. Ohne ihn lässt sich bei einer Teilerstattung // nicht erkennen, warum nur ein Teil zurückgeht -- und die Zusicherung „ich habe den Betrag // beglichen" bliebe unbelegt, obwohl mareike ihn kennt. ['Gezahlter Teilnahmebeitrag', $this->money($participant->amount_paid?->getAmount() ?? 0.0)], ['Rechnung', e($this->invoiceNumber())], ['Erstattungsbetrag', $this->money($refund->amount?->getAmount() ?? 0.0)], ['Grund', e($refund->reasonLabel())], ]; // Bei einem Freitext-Grund ist die Begründung der Text der Aktionsleitung, sonst der des // Katalogs. Fehlt beides, entfällt die Zeile -- eine Beschriftung ohne Wert sieht nach Fehler aus. $reasonText = trim($refund->reasonText()); if ($reasonText !== '') { $rows[] = ['Begründung', e($reasonText)]; } $rows[] = ['Kontoinhaber*in', e((string) $refund->account_owner)]; $rows[] = ['IBAN', e($this->formatIban((string) $refund->account_iban))]; $html = ''; foreach ($rows as [$key, $value]) { $html .= sprintf( '%s%s', e($key), $value ); } return '' . $html . '
'; } /** IBAN in Vierergruppen -- so steht sie auf jedem Beleg und lässt sich abtippen. */ private function formatIban(string $iban): string { return trim(chunk_split($iban, 4, ' ')); } private function money(float $value): string { return new Amount($value, 'Euro')->getFormattedAmount() . ' €'; } }