billing: Rechnungsversand — PDF im Anhang der Zahlungsmail, optionale Rechnungs-E-Mail, Stripe-Rechnungs-PDF direkt aus dem Kundenbereich
Kundenwunsch 06.10.2026: Ein Kunde kam über die Stripe-Rechnungsseite nicht an seine Rechnung und fragte nach Belegen und einer Rechnungs-E-Mail. - Profil ▸ Rechnungsadresse ▸ Rechnungsversand: Schalter „Rechnungs-PDF an die Konto-E-Mail" (Standard an), optionale „E-Mail für Rechnungen" mit Schalter „Rechnungen als PDF an diese Adresse senden" (Standard an). Neue Spalten an billing_addresses (klein, ohne FULLTEXT). - InvoiceDelivery entscheidet Empfänger und Anhang; gleiche Adresse wie das Konto ergibt eine Mail mit PDF. - Zahlungsbestätigung (STR) und Bestandskunden-Rechnung/-Erinnerung (MAN) folgen der Einstellung; die Fassung an die Rechnungs-E-Mail nennt das Konto, ohne Blindkopie und ohne Login-Button. - StripeInvoicePdf holt das Stripe-PDF einmal und legt es lokal ab (storage/app/private/invoices/stripe); Mailversand wiederholt bei Stripe-Ausfall (5 Versuche). - Rechnungsseite: „PDF öffnen" aus pressekonto, „Beleg bei Stripe" für den Zahlungsbeleg auf der Stripe-Seite. Suite 2.340 grün, einer übersprungen; Larastan und Pint ohne Meldung. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
da9411bdab
commit
6d03d8de39
18 changed files with 695 additions and 30 deletions
|
|
@ -5,22 +5,39 @@
|
|||
$title = $isReminder ? 'Zahlungserinnerung zu Ihrer Rechnung' : 'Ihre Rechnung für den Bestandsschutz';
|
||||
$preheader = $isReminder
|
||||
? 'Für die Rechnung '.$invoice->number.' liegt uns noch kein Zahlungseingang vor.'
|
||||
: 'Rechnung '.$invoice->number.' im Anhang — bitte bis zum '.$dueDate.' überweisen.';
|
||||
: 'Rechnung '.$invoice->number.($attachPdf ? ' im Anhang' : ' im Konto').' — bitte bis zum '.$dueDate.' überweisen.';
|
||||
@endphp
|
||||
<x-emails.layout :title="$title" :preheader="$preheader">
|
||||
<p>Guten Tag {{ $user->name }},</p>
|
||||
@if ($forInvoiceRecipient)
|
||||
<p>Guten Tag,</p>
|
||||
|
||||
<p>
|
||||
diese Rechnung betrifft das Konto von {{ $user->name }} ({{ $user->email }}) auf
|
||||
pressekonto. Diese Adresse ist dort als Empfänger für Rechnungen hinterlegt.
|
||||
</p>
|
||||
@else
|
||||
<p>Guten Tag {{ $user->name }},</p>
|
||||
@endif
|
||||
|
||||
@if ($isReminder)
|
||||
<p>
|
||||
zu der unten aufgeführten Rechnung konnten wir bisher keinen Zahlungseingang
|
||||
feststellen. Vielleicht ist sie in der Ablage untergegangen — die Rechnung liegt
|
||||
dieser E-Mail noch einmal als PDF bei.
|
||||
feststellen. Vielleicht ist sie in der Ablage untergegangen —
|
||||
@if ($attachPdf)
|
||||
die Rechnung liegt dieser E-Mail noch einmal als PDF bei.
|
||||
@else
|
||||
die Rechnung finden Sie in Ihrem Konto unter Rechnungen.
|
||||
@endif
|
||||
</p>
|
||||
@else
|
||||
<p>
|
||||
als Bestandskunde nutzen Sie Ihre bisherige Vereinbarung auf pressekonto weiter
|
||||
zu den gewohnten Konditionen (Bestandsschutz). Für den neuen Leistungszeitraum
|
||||
erhalten Sie anbei Ihre Rechnung als PDF.
|
||||
@if ($attachPdf)
|
||||
erhalten Sie anbei Ihre Rechnung als PDF.
|
||||
@else
|
||||
finden Sie Ihre Rechnung in Ihrem Konto unter Rechnungen.
|
||||
@endif
|
||||
</p>
|
||||
@if ($invoice->replaces_legacy_invoice_number)
|
||||
<p>
|
||||
|
|
@ -79,7 +96,9 @@
|
|||
</p>
|
||||
</div>
|
||||
|
||||
<x-emails.button :url="$invoicesUrl">Rechnung im Konto ansehen</x-emails.button>
|
||||
@unless ($forInvoiceRecipient)
|
||||
<x-emails.button :url="$invoicesUrl">Rechnung im Konto ansehen</x-emails.button>
|
||||
@endunless
|
||||
|
||||
<div class="note">
|
||||
<p>
|
||||
|
|
|
|||
|
|
@ -3,12 +3,22 @@
|
|||
.' '.($invoice->currency === 'EUR' ? '€' : $invoice->currency);
|
||||
@endphp
|
||||
<x-emails.layout title="Zahlungsbestätigung für Ihre Buchung" preheader="Wir haben Ihre Zahlung erhalten — vielen Dank für Ihre Buchung.">
|
||||
<p>Hallo {{ $user->name }},</p>
|
||||
@if ($forInvoiceRecipient)
|
||||
<p>Guten Tag,</p>
|
||||
|
||||
<p>
|
||||
vielen Dank für Ihre Buchung. Wir haben Ihre Zahlung erhalten und
|
||||
die zugehörige Rechnung in Ihrem Konto hinterlegt:
|
||||
</p>
|
||||
<p>
|
||||
im Auftrag von {{ $user->name }} ({{ $user->email }}) erhalten Sie
|
||||
die Rechnung zu einer bezahlten Buchung auf pressekonto. Diese
|
||||
Adresse ist im Konto als Empfänger für Rechnungen hinterlegt.
|
||||
</p>
|
||||
@else
|
||||
<p>Hallo {{ $user->name }},</p>
|
||||
|
||||
<p>
|
||||
vielen Dank für Ihre Buchung. Wir haben Ihre Zahlung erhalten und
|
||||
die zugehörige Rechnung in Ihrem Konto hinterlegt:
|
||||
</p>
|
||||
@endif
|
||||
|
||||
<dl class="meta">
|
||||
<dt>Rechnungsnummer</dt>
|
||||
|
|
@ -25,10 +35,20 @@
|
|||
<dd>{{ $formatCents($invoice->total_cents) }}</dd>
|
||||
</dl>
|
||||
|
||||
<x-emails.button :url="$invoicesUrl">Rechnung im Konto ansehen</x-emails.button>
|
||||
@if ($attachPdf)
|
||||
<p>Die Rechnung finden Sie als PDF im Anhang dieser E-Mail.</p>
|
||||
@endif
|
||||
|
||||
@unless ($forInvoiceRecipient)
|
||||
<x-emails.button :url="$invoicesUrl">Rechnung im Konto ansehen</x-emails.button>
|
||||
@endunless
|
||||
|
||||
<div class="note">
|
||||
<p>Alle Rechnungen und Belege finden Sie jederzeit in Ihrem Konto unter Rechnungen.</p>
|
||||
@if ($forInvoiceRecipient)
|
||||
<p>Den Versand an diese Adresse kann der Kontoinhaber im Profil unter Rechnungsadresse ändern oder abstellen.</p>
|
||||
@else
|
||||
<p>Alle Rechnungen und Belege finden Sie jederzeit in Ihrem Konto unter Rechnungen. Ob das PDF anhängt und ob Rechnungen zusätzlich an eine eigene Rechnungs-E-Mail gehen, legen Sie im Profil unter Rechnungsadresse fest.</p>
|
||||
@endif
|
||||
<p>Bei Fragen erreichen Sie uns über den <a href="{{ config('domains.domain_portal_url') }}/de/hilfe">Hilfe-Bereich auf pressekonto</a>.</p>
|
||||
</div>
|
||||
</x-emails.layout>
|
||||
|
|
|
|||
|
|
@ -169,15 +169,28 @@ new #[Layout('components.layouts.app'), Title('Rechnungen')] class extends Compo
|
|||
</flux:table.cell>
|
||||
<flux:table.cell>
|
||||
@if ($invoice->stripe_invoice_id)
|
||||
<flux:button
|
||||
size="sm"
|
||||
variant="filled"
|
||||
icon="arrow-top-right-on-square"
|
||||
:href="route('me.invoices.stripe', $invoice)"
|
||||
target="_blank"
|
||||
>
|
||||
{{ __('Bei Stripe öffnen') }}
|
||||
</flux:button>
|
||||
{{-- PDF kommt aus pressekonto; die Stripe-Seite bleibt für den Zahlungsbeleg. --}}
|
||||
<div class="flex items-center gap-2">
|
||||
<flux:button
|
||||
size="sm"
|
||||
variant="filled"
|
||||
icon="document-text"
|
||||
:href="route('me.invoices.stripe-pdf', $invoice)"
|
||||
target="_blank"
|
||||
>
|
||||
{{ __('PDF öffnen') }}
|
||||
</flux:button>
|
||||
<flux:button
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
icon="arrow-top-right-on-square"
|
||||
:href="route('me.invoices.stripe', $invoice)"
|
||||
target="_blank"
|
||||
:tooltip="__('Rechnung und Zahlungsbeleg auf der Stripe-Seite')"
|
||||
>
|
||||
{{ __('Beleg bei Stripe') }}
|
||||
</flux:button>
|
||||
</div>
|
||||
@else
|
||||
<flux:button
|
||||
size="sm"
|
||||
|
|
|
|||
|
|
@ -57,6 +57,16 @@ new #[Layout('components.layouts.app'), Title('Mein Profil')] class extends Comp
|
|||
|
||||
public string $billingCountryCode = 'DE';
|
||||
|
||||
/**
|
||||
* Rechnungsversand (06.10.2026): optionale Rechnungs-E-Mail und je
|
||||
* Empfänger, ob das Rechnungs-PDF anhängt (siehe InvoiceDelivery).
|
||||
*/
|
||||
public string $invoiceEmail = '';
|
||||
|
||||
public bool $invoicePdfToAccount = true;
|
||||
|
||||
public bool $invoicePdfToInvoiceEmail = true;
|
||||
|
||||
public ?string $vatCheckStatus = null;
|
||||
|
||||
public ?string $vatCheckMessage = null;
|
||||
|
|
@ -101,6 +111,9 @@ new #[Layout('components.layouts.app'), Title('Mein Profil')] class extends Comp
|
|||
$this->billingPostalCode = (string) ($billingAddress?->postal_code ?? '');
|
||||
$this->billingCity = (string) ($billingAddress?->city ?? '');
|
||||
$this->billingCountryCode = (string) ($billingAddress?->country_code ?? 'DE');
|
||||
$this->invoiceEmail = (string) ($billingAddress?->invoice_email ?? '');
|
||||
$this->invoicePdfToAccount = (bool) ($billingAddress?->invoice_pdf_to_account ?? true);
|
||||
$this->invoicePdfToInvoiceEmail = (bool) ($billingAddress?->invoice_pdf_to_invoice_email ?? true);
|
||||
|
||||
// Bestandsdaten vor der Feld-Trennung: `name` war eine freie
|
||||
// Empfängerzeile — einmalig in Vor-/Nachname aufteilen.
|
||||
|
|
@ -185,6 +198,9 @@ new #[Layout('components.layouts.app'), Title('Mein Profil')] class extends Comp
|
|||
'billingPostalCode' => ['nullable', 'string', 'max:20'],
|
||||
'billingCity' => ['nullable', 'string', 'max:120'],
|
||||
'billingCountryCode' => ['nullable', 'string', 'size:2', Rule::in(array_keys((array) config('countries.items', [])))],
|
||||
'invoiceEmail' => ['nullable', 'email', 'max:255'],
|
||||
'invoicePdfToAccount' => ['boolean'],
|
||||
'invoicePdfToInvoiceEmail' => ['boolean'],
|
||||
];
|
||||
|
||||
// Sobald irgendein Rechnungsfeld gefüllt ist, werden die
|
||||
|
|
@ -205,6 +221,7 @@ new #[Layout('components.layouts.app'), Title('Mein Profil')] class extends Comp
|
|||
'billingPostalCode' => __('PLZ'),
|
||||
'billingCity' => __('Ort'),
|
||||
'billingCountryCode' => __('Land'),
|
||||
'invoiceEmail' => __('E-Mail für Rechnungen'),
|
||||
]);
|
||||
|
||||
// USt-ID: hartes Format-Gate; die Online-Bestätigung (eVatR) bleibt
|
||||
|
|
@ -266,6 +283,9 @@ new #[Layout('components.layouts.app'), Title('Mein Profil')] class extends Comp
|
|||
// pro Rechnung eingefroren und bestimmt die Steuer
|
||||
// (EU-Befreiung nur mit gültiger USt-ID).
|
||||
'vat_id' => $validated['taxIdNumber'] ?: null,
|
||||
'invoice_email' => $validated['invoiceEmail'] ?: null,
|
||||
'invoice_pdf_to_account' => $validated['invoicePdfToAccount'],
|
||||
'invoice_pdf_to_invoice_email' => $validated['invoicePdfToInvoiceEmail'],
|
||||
],
|
||||
);
|
||||
}
|
||||
|
|
@ -307,7 +327,8 @@ new #[Layout('components.layouts.app'), Title('Mein Profil')] class extends Comp
|
|||
|| filled($this->billingAddress1)
|
||||
|| filled($this->billingAddress2)
|
||||
|| filled($this->billingPostalCode)
|
||||
|| filled($this->billingCity);
|
||||
|| filled($this->billingCity)
|
||||
|| filled($this->invoiceEmail);
|
||||
}
|
||||
|
||||
public function billingIsComplete(): bool
|
||||
|
|
@ -525,6 +546,35 @@ new #[Layout('components.layouts.app'), Title('Mein Profil')] class extends Comp
|
|||
@endif
|
||||
@endif
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div class="text-[11px] font-semibold tracking-[0.14em] uppercase text-[color:var(--color-ink-3)] mb-3">
|
||||
{{ __('Rechnungsversand') }}
|
||||
</div>
|
||||
<div class="space-y-4">
|
||||
<flux:switch
|
||||
wire:model="invoicePdfToAccount"
|
||||
align="right"
|
||||
:label="__('Rechnungs-PDF an die Konto-E-Mail')"
|
||||
:description="__('Die Zahlungsbestätigung geht immer an :email. Eingeschaltet hängt die Rechnung als PDF an.', ['email' => $user->email])"
|
||||
/>
|
||||
<flux:input
|
||||
wire:model.live.blur="invoiceEmail"
|
||||
type="email"
|
||||
:label="__('E-Mail für Rechnungen (optional)')"
|
||||
placeholder="buchhaltung@firma.de"
|
||||
:description="__('Zum Beispiel die Buchhaltung. Diese Adresse bekommt nur Rechnungen, keine anderen Nachrichten.')"
|
||||
/>
|
||||
@if (filled($invoiceEmail))
|
||||
<flux:switch
|
||||
wire:model="invoicePdfToInvoiceEmail"
|
||||
align="right"
|
||||
:label="__('Rechnungen als PDF an diese Adresse senden')"
|
||||
:description="__('Jede neue Rechnung geht zusätzlich mit PDF an :email.', ['email' => $invoiceEmail])"
|
||||
/>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="px-5 py-4 border-t border-[color:var(--color-bg-rule)] flex justify-end">
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue