Verkauf: Stornorechnung — fehlende createCancellation() im InvoiceRepository ergänzt, eigene Storno-Mail

Der SalesController rief InvoiceRepository::createCancellation() auf, die Methode existierte aber nicht — jeder Storno-Versuch endete im 500er. Die Stornorechnung wird jetzt aus demselben Nummernkreis wie die Rechnung erzeugt (pdf.cancellation_invoice, Rechnungs-Briefpapier), als cancellation_invoice am Auftrag gespeichert und der Zähler erhöht.

Optional geht eine Storno-Mail mit angehängter Stornorechnung (MailCancellation, Texte email.cancellation_title/_copy1line) statt der Rechnungs-Mail an den Kunden.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
Kevin Adametz 2026-09-09 12:03:35 +00:00
parent be29be863f
commit 9c362f14d9
4 changed files with 254 additions and 95 deletions

View file

@ -0,0 +1,47 @@
<?php
namespace App\Mail;
use App\Models\ShoppingOrder;
use App\Services\Invoice;
use Illuminate\Bus\Queueable;
use Illuminate\Mail\Mailable;
use Illuminate\Queue\SerializesModels;
use Storage;
class MailCancellation extends Mailable
{
use Queueable, SerializesModels;
protected $shopping_order;
public $subject;
public function __construct(ShoppingOrder $shopping_order)
{
$this->shopping_order = $shopping_order;
$this->subject = 'Stornorechnung zu Deiner Bestellung: '.$shopping_order->getLastShoppingPayment('reference');
}
public function build()
{
$title = __('email.cancellation_title');
$copy1line = __('email.cancellation_copy1line').Invoice::getNumber($this->shopping_order);
$filename = Invoice::getCancellationFilename($this->shopping_order);
$path = Invoice::getCancellationDownloadPath($this->shopping_order);
if (! Storage::disk('public')->exists($path)) {
return;
}
$file = Storage::disk('public')->path($path);
$mime = Storage::disk('public')->mimeType($path);
return $this->view('emails.blank')->with([
'title' => $title,
'copy1line' => $copy1line,
])->attach($file, [
'as' => $filename,
'mime' => $mime,
]);
}
}

View file

@ -25,6 +25,14 @@ class InvoiceRepository extends BaseRepository
private $delivery_filename;
private $cancellation_date;
private $cancellation_number;
private $cancellation_filename;
private $cancellation_dir;
public function __construct(ShoppingOrder $model)
{
$this->model = $model;
@ -116,6 +124,84 @@ class InvoiceRepository extends BaseRepository
return null;
}
/**
* Stornorechnung zu einer bereits erstellten Rechnung erzeugen (Fix 08.09.2026:
* die Methode wurde vom SalesController aufgerufen, existierte aber nie —
* jeder Storno-Versuch lief in einen 500er).
*
* Die Stornonummer wird aus demselben Zähler wie die Rechnungsnummer gezogen
* (das Modal zeigt "nächste Rechnungsnummer" an) und der Zähler danach erhöht.
*
* @return bool true = Stornorechnung erstellt
*/
public function createCancellation($request = [])
{
// ohne Ursprungsrechnung gibt es nichts zu stornieren
if (! Invoice::isInvoice($this->model)) {
return false;
}
// Doppel-Storno verhindern (der Controller prüft das ebenfalls)
if (Invoice::isCancellationInvoice($this->model)) {
return false;
}
$number = Invoice::getInvoiceNumber();
$this->cancellation_date = isset($request['cancellation_date']) ? $request['cancellation_date'] : now()->format('d.m.Y');
$cancellation_send_mail = isset($request['cancellation_send_mail']) ? true : false;
$this->cancellation_number = Invoice::createInvoiceNumber($number, $this->cancellation_date);
$this->cancellation_dir = Invoice::getCancellationStorageDir($this->cancellation_date);
$this->cancellation_filename = Invoice::makeCancellationFilename($this->cancellation_number);
$this->makeCancellationPDF();
$this->model->cancellation_invoice = [
'filename' => $this->cancellation_filename,
'dir' => $this->cancellation_dir,
'disk' => 'public',
'cancellation_number' => $this->cancellation_number,
'cancellation_date' => $this->cancellation_date,
'invoice_number' => Invoice::getNumber($this->model),
'invoice_date' => Invoice::getDate($this->model),
];
$this->model->save();
Invoice::makeNextInvoiceNumber();
if ($cancellation_send_mail) {
Invoice::sendCancellationMail($this->model);
}
return true;
}
private function makeCancellationPDF()
{
$data = [
'shopping_order' => $this->model,
'cancellation_date' => $this->cancellation_date,
'cancellation_number' => $this->cancellation_number,
'original_invoice_number' => Invoice::getNumber($this->model),
'original_invoice_date' => Invoice::getDate($this->model),
];
if ($this->model->auth_user_id) {
UserService::checkUserTaxShippingCountry($this->model->auth_user, $this->model->country_id);
$data = array_merge($data, UserService::getYardInfo());
}
if (! Storage::disk('public')->exists($this->cancellation_dir)) {
Storage::disk('public')->makeDirectory($this->cancellation_dir);
}
$path = Storage::disk('public')->path('');
$pdf_file = new InvoicePDF('pdf.cancellation_invoice');
$pdf_file->create($data, $this->cancellation_filename, 'save', $path.$this->cancellation_dir);
$pdfMerger = new MyPDFMerger;
$pdfMerger->addPDF($path.$this->cancellation_dir.$this->cancellation_filename);
// eigenes Storno-Briefpapier gibt es nicht, daher das Rechnungs-Template
$file = $pdfMerger->myMerge('string', $this->cancellation_filename, 'template_invoice_de');
Storage::disk('public')->put($this->cancellation_dir.$this->cancellation_filename, $file);
}
/**
* Altdaten-Reparatur an EINER Stelle (Review 20.07.2026): Rechnung und/oder
* Lieferschein fehlen auf der Platte, obwohl die DB-Metadaten existieren

View file

@ -2,6 +2,7 @@
namespace App\Services;
use App\Mail\MailCancellation;
use App\Mail\MailInvoice;
use App\Mail\MailLogistic;
use App\Models\Setting;
@ -191,6 +192,29 @@ class Invoice
Mail::to($billing_email)->bcc($bcc)->send(new MailInvoice($shopping_order));
}
/**
* Storno-Mail mit angehängter Stornorechnung (Audit 3.1: vorher hätte hier die
* Rechnungs-Mail gestanden — der Kunde bekäme bei einer Stornierung die Rechnung).
*/
public static function sendCancellationMail($shopping_order)
{
$bcc = [];
$billing_email = $shopping_order->shopping_user->billing_email;
if (! $billing_email) {
if ($shopping_order->mode === 'test') {
$billing_email = config('app.checkout_test_mail');
} else {
$billing_email = config('app.checkout_mail');
}
}
if ($shopping_order->mode === 'test') {
$bcc[] = config('app.checkout_test_mail');
} else {
$bcc[] = config('app.checkout_mail');
}
Mail::to($billing_email)->bcc($bcc)->send(new MailCancellation($shopping_order));
}
public static function sendLogisticMail(ShoppingOrder $shopping_order)
{
$to = [config('app.logistic_mail')]; // ['versand@aloe-vera.bio'];

View file

@ -1,96 +1,98 @@
<?php
<?php
return array (
'reset_passwort' => 'Passwort zurücksetzen',
'mail_confirm' => 'E-Mail bestätigen',
'subject_activate' => 'Account aktivieren',
'account_active' => 'Account freigeschaltet',
'subject_reset' => 'Passwort zurücksetzen',
'dear_mrs' => 'Hallo',
'dear_sir' => 'Hallo',
'hello' => 'Hallo',
'greetings' => 'Mit freundlichen Grüßen',
'sender' => 'Dein Team von Grüne Seele',
'request_from' => 'Anfrage von Grüne Seele',
'your_request_from' => 'Deine Anfrage von',
'checkout_subject' => 'Deine Bestellung',
'checkout_subject_paid' => 'Zahlungsbestätigung - Deine Bestellung',
'checkout_subject_non_paid' => 'Bestätigung - Deine Bestellung',
'checkout_subject_extern' => 'Neue Bestellung',
'change_e_mail' => 'E-Mail Adresse ändern',
'salutation' => 'Hallo',
'first_name' => 'Vorname',
'last_name' => 'Nachname',
'email' => 'E-Mail',
'phone' => 'Telefon',
'subject' => 'Betreff',
'message' => 'Nachricht',
'sales_partnership' => 'Vertriebspartnerschaft',
'sales_partnership_message' => 'Hinweis',
'button_account' => 'zum Account',
'active_copy1line' => 'Du erhältst diese E-Mail, weil wir eine Anfrage zur Änderung Deiner E-Mail-Adresse für Dein Konto erhalten haben.',
'copy2line' => 'Oder kopiere diesen Link in die Adressleiste Deines Browsers.',
'copy3line' => 'Für weitere Fragen stehen wir Dir gerne zur Verfügung.',
'email_verify' => 'Bestätige Deine E-Mail-Adresse',
'email_subject' => 'Nachricht von Grüne Seele',
'email_incomplete' => 'Deine Daten sind nicht vollständig.',
'account_incomplete_copy1line' => 'Dein Account wurde nicht freigeschaltet, Deine Daten sind nicht vollständig. Bitte folge dem unten stehenden Link um Deine Daten zu vervollständigen. ',
'verify_e_mail' => 'Bestätige Deine Daten und E-Mail-Adresse',
'verify_copy1line' => 'Wir haben Daten über Dich in unserem System gespeichert. Bitte folge dem unten stehenden Link um Deine E-Mail-Adresse zu bestätigen. Weitherhin kannst Du Deine Daten ändern oder auch löschen.',
'email_verify_copy1line' => 'Vielen Dank für Deine Registrierung. Bitte folge dem unten stehenden Link, um Deine E-Mail-Adresse zu bestätigen.',
'copy_to_browser' => 'Oder kopiere diesen Link in die Adressleiste Deines Browsers.',
'activate_copy' => 'Bitte bestätige Deine E-Mail und aktiviere Deinen Account über diesen Link:',
'account_active_copy1line' => 'Dein Account wurde nach erfolgreicher Prüfung freigeschaltet. Deinen Beratervertag findest du auf der Startseite im Loginbereich von partner.gruene-seele.bio Melde dich mit deinen Benutzerdaten an, um weitere Schritte zu unternehmen.',
'reset_pass_copy1line' => 'Du erhälst diese E-Mail, weil wir eine Anfrage zum Zurücksetzen Deines Passworts für Dein Konto erhalten haben.',
'checkout_copy1line' => 'vielen Dank für Deine Bestellung bei Grüne Seele. Nachfolgend haben wir zur Kontrolle Deine Bestellung noch einmal aufgelistet.',
'checkout_copy3line' => 'Bei Fragen sind wir jederzeit für Dich da.',
'checkout_copy3line_extern' => 'Bestellung über Berater:',
'status_copy1line' => 'Status zu Deiner Bestellung auf Grüne Seele',
'invoice_title' => 'Rechnung zu Deiner Bestellung auf Grüne Seele',
'credit_title' => 'Gutschrift aus Vertriebspartnerbestellungen auf Grüne Seele',
'invoice_copy1line' => 'vielen Dank für Deine Bestellung bei Grüne Seele. Nachfolgend senden wir Dir die Rechnung zu deiner Bestellung: ',
'credit_copy1line' => 'vielen Dank für Deine Vertriebspartnerschaft bei Grüne Seele. Nachfolgend senden wir Dir Deine Gutschrift zu. Der Betrag wird zeitnah angewiesen, bitte prüfen deine Zahlungsdaten in deinem Konto: <a href="https://partner.gruene-seele.bio/user/edit">Mein Konto -> Meine Daten</a>.',
'footer_copy1' => 'GRÜNE SEELE GmbH | Hauptstr. 174 | 51143 Köln | Telefon: (+49) 2203 183 86 14 | E-Mail: service@gruene-seele.bio',
'footer_copy2' => '',
'footer_copy3' => '© 2022 All Rights Reserved',
'checkout_mail_hl1' => 'Du hast folgende Artikel bestellt:',
'checkout_mail_shipping' => 'Verpackungs- u. Versandkosten',
'checkout_mail_status_info' => 'Statusinfo:',
'checkout_mail_subtotal_ws' => 'Summe ohne MwSt.',
'checkout_mail_tax' => 'zzgl. MwSt',
'checkout_mail_tax_19' => 'zzgl. 19% MwSt',
'checkout_mail_tax_7' => 'zzgl. 7% MwSt',
'checkout_mail_total' => 'Gesamtpreis',
'checkout_mail_tax_info' => 'Preis inkl. MwSt',
'checkout_mail_pay_info' => 'Zahlungsinfo:',
'checkout_mail_pay_success' => 'Zahlung ist bestätigt!',
'checkout_mail_pay_with' => 'Zahlung mit:',
'checkout_mail_pay_ref' => 'Zahlungsreferenz:',
'checkout_mail_your_mail' => 'Deine E-Mai:',
'checkout_mail_invoice_addess' => 'Deine Rechnungsadresse:',
'checkout_mail_deliver_addess' => 'Deine Lieferadresse:',
'checkout_mail_pickup_addess' => 'Ich hole die Ware persönlich ab:',
'checkout_mail_deliver_customer' => 'Lieferadresse des Kunden:',
'checkout_mail_order_for_me' => 'Beraterbestellung für Dich:',
'checkout_mail_order_for_ot' => 'Beraterbestellung für Deinen Kunden:',
'checkout_mail_order_for_extern' => 'Kundenbestellung über externen Shop:',
'checkout_mail_order_for_wizard' => 'Beraterbestellung zur Deiner Registrierung:',
'checkout_mail_order_for_membership' => 'Beraterbestellung für Deine Mitgliedschaft:',
'checkout_mail_same_address' => 'Lieferadresse ist gleich Rechnungsadresse',
'checkout_mail_pay_error' => 'Die Zahlung ist fehlgeschlagen!',
'checkout_mail_pay_non' => 'Keine Zahlung erhalten!',
'checkout_mail_pay_pre' => 'Bezahlung per Vorkasse:',
'checkout_mail_pay_pre_c1' => 'Bitte überweise',
'checkout_mail_pay_pre_c2' => 'EUR auf die folgende Kontoverbindung, um den Kauf abzuschließen.',
'checkout_mail_bank_holder' => 'Kontoinhaber:',
'checkout_mail_bank_iban' => 'IBAN:',
'checkout_mail_bank_bic' => 'BIC:',
'checkout_mail_bank_name' => 'Bank',
'checkout_mail_bank_total' => 'Gesamtbetrag',
'checkout_mail_bank_code' => 'Verwendungszweck',
'checkout_mail_pay_approved' => 'Deine Zahlung wurde genehmigt, eine Bestätigung der Zahlung wird automatisch erstellt.',
'checkout_mail_pay_invoice_open' => 'Deine Zahlung per Rechnung wurde genehmigt.',
'checkout_mail_pay_non' => 'Deine Bestellung wurde ausgeführt.',
'checkout_mail_system_status' => 'SystemStatus:',
'my_orders' => 'Meine Bestellungen',
);
return [
'reset_passwort' => 'Passwort zurücksetzen',
'mail_confirm' => 'E-Mail bestätigen',
'subject_activate' => 'Account aktivieren',
'account_active' => 'Account freigeschaltet',
'subject_reset' => 'Passwort zurücksetzen',
'dear_mrs' => 'Hallo',
'dear_sir' => 'Hallo',
'hello' => 'Hallo',
'greetings' => 'Mit freundlichen Grüßen',
'sender' => 'Dein Team von Grüne Seele',
'request_from' => 'Anfrage von Grüne Seele',
'your_request_from' => 'Deine Anfrage von',
'checkout_subject' => 'Deine Bestellung',
'checkout_subject_paid' => 'Zahlungsbestätigung - Deine Bestellung',
'checkout_subject_non_paid' => 'Bestätigung - Deine Bestellung',
'checkout_subject_extern' => 'Neue Bestellung',
'change_e_mail' => 'E-Mail Adresse ändern',
'salutation' => 'Hallo',
'first_name' => 'Vorname',
'last_name' => 'Nachname',
'email' => 'E-Mail',
'phone' => 'Telefon',
'subject' => 'Betreff',
'message' => 'Nachricht',
'sales_partnership' => 'Vertriebspartnerschaft',
'sales_partnership_message' => 'Hinweis',
'button_account' => 'zum Account',
'active_copy1line' => 'Du erhältst diese E-Mail, weil wir eine Anfrage zur Änderung Deiner E-Mail-Adresse für Dein Konto erhalten haben.',
'copy2line' => 'Oder kopiere diesen Link in die Adressleiste Deines Browsers.',
'copy3line' => 'Für weitere Fragen stehen wir Dir gerne zur Verfügung.',
'email_verify' => 'Bestätige Deine E-Mail-Adresse',
'email_subject' => 'Nachricht von Grüne Seele',
'email_incomplete' => 'Deine Daten sind nicht vollständig.',
'account_incomplete_copy1line' => 'Dein Account wurde nicht freigeschaltet, Deine Daten sind nicht vollständig. Bitte folge dem unten stehenden Link um Deine Daten zu vervollständigen. ',
'verify_e_mail' => 'Bestätige Deine Daten und E-Mail-Adresse',
'verify_copy1line' => 'Wir haben Daten über Dich in unserem System gespeichert. Bitte folge dem unten stehenden Link um Deine E-Mail-Adresse zu bestätigen. Weitherhin kannst Du Deine Daten ändern oder auch löschen.',
'email_verify_copy1line' => 'Vielen Dank für Deine Registrierung. Bitte folge dem unten stehenden Link, um Deine E-Mail-Adresse zu bestätigen.',
'copy_to_browser' => 'Oder kopiere diesen Link in die Adressleiste Deines Browsers.',
'activate_copy' => 'Bitte bestätige Deine E-Mail und aktiviere Deinen Account über diesen Link:',
'account_active_copy1line' => 'Dein Account wurde nach erfolgreicher Prüfung freigeschaltet. Deinen Beratervertag findest du auf der Startseite im Loginbereich von partner.gruene-seele.bio Melde dich mit deinen Benutzerdaten an, um weitere Schritte zu unternehmen.',
'reset_pass_copy1line' => 'Du erhälst diese E-Mail, weil wir eine Anfrage zum Zurücksetzen Deines Passworts für Dein Konto erhalten haben.',
'checkout_copy1line' => 'vielen Dank für Deine Bestellung bei Grüne Seele. Nachfolgend haben wir zur Kontrolle Deine Bestellung noch einmal aufgelistet.',
'checkout_copy3line' => 'Bei Fragen sind wir jederzeit für Dich da.',
'checkout_copy3line_extern' => 'Bestellung über Berater:',
'status_copy1line' => 'Status zu Deiner Bestellung auf Grüne Seele',
'invoice_title' => 'Rechnung zu Deiner Bestellung auf Grüne Seele',
'cancellation_title' => 'Stornorechnung zu Deiner Bestellung auf Grüne Seele',
'cancellation_copy1line' => 'anbei erhältst Du die Stornorechnung zu Deiner Rechnung Nr. ',
'credit_title' => 'Gutschrift aus Vertriebspartnerbestellungen auf Grüne Seele',
'invoice_copy1line' => 'vielen Dank für Deine Bestellung bei Grüne Seele. Nachfolgend senden wir Dir die Rechnung zu deiner Bestellung: ',
'credit_copy1line' => 'vielen Dank für Deine Vertriebspartnerschaft bei Grüne Seele. Nachfolgend senden wir Dir Deine Gutschrift zu. Der Betrag wird zeitnah angewiesen, bitte prüfen deine Zahlungsdaten in deinem Konto: <a href="https://partner.gruene-seele.bio/user/edit">Mein Konto -> Meine Daten</a>.',
'footer_copy1' => 'GRÜNE SEELE GmbH | Hauptstr. 174 | 51143 Köln | Telefon: (+49) 2203 183 86 14 | E-Mail: service@gruene-seele.bio',
'footer_copy2' => '',
'footer_copy3' => '© 2022 All Rights Reserved',
'checkout_mail_hl1' => 'Du hast folgende Artikel bestellt:',
'checkout_mail_shipping' => 'Verpackungs- u. Versandkosten',
'checkout_mail_status_info' => 'Statusinfo:',
'checkout_mail_subtotal_ws' => 'Summe ohne MwSt.',
'checkout_mail_tax' => 'zzgl. MwSt',
'checkout_mail_tax_19' => 'zzgl. 19% MwSt',
'checkout_mail_tax_7' => 'zzgl. 7% MwSt',
'checkout_mail_total' => 'Gesamtpreis',
'checkout_mail_tax_info' => 'Preis inkl. MwSt',
'checkout_mail_pay_info' => 'Zahlungsinfo:',
'checkout_mail_pay_success' => 'Zahlung ist bestätigt!',
'checkout_mail_pay_with' => 'Zahlung mit:',
'checkout_mail_pay_ref' => 'Zahlungsreferenz:',
'checkout_mail_your_mail' => 'Deine E-Mai:',
'checkout_mail_invoice_addess' => 'Deine Rechnungsadresse:',
'checkout_mail_deliver_addess' => 'Deine Lieferadresse:',
'checkout_mail_pickup_addess' => 'Ich hole die Ware persönlich ab:',
'checkout_mail_deliver_customer' => 'Lieferadresse des Kunden:',
'checkout_mail_order_for_me' => 'Beraterbestellung für Dich:',
'checkout_mail_order_for_ot' => 'Beraterbestellung für Deinen Kunden:',
'checkout_mail_order_for_extern' => 'Kundenbestellung über externen Shop:',
'checkout_mail_order_for_wizard' => 'Beraterbestellung zur Deiner Registrierung:',
'checkout_mail_order_for_membership' => 'Beraterbestellung für Deine Mitgliedschaft:',
'checkout_mail_same_address' => 'Lieferadresse ist gleich Rechnungsadresse',
'checkout_mail_pay_error' => 'Die Zahlung ist fehlgeschlagen!',
'checkout_mail_pay_non' => 'Keine Zahlung erhalten!',
'checkout_mail_pay_pre' => 'Bezahlung per Vorkasse:',
'checkout_mail_pay_pre_c1' => 'Bitte überweise',
'checkout_mail_pay_pre_c2' => 'EUR auf die folgende Kontoverbindung, um den Kauf abzuschließen.',
'checkout_mail_bank_holder' => 'Kontoinhaber:',
'checkout_mail_bank_iban' => 'IBAN:',
'checkout_mail_bank_bic' => 'BIC:',
'checkout_mail_bank_name' => 'Bank',
'checkout_mail_bank_total' => 'Gesamtbetrag',
'checkout_mail_bank_code' => 'Verwendungszweck',
'checkout_mail_pay_approved' => 'Deine Zahlung wurde genehmigt, eine Bestätigung der Zahlung wird automatisch erstellt.',
'checkout_mail_pay_invoice_open' => 'Deine Zahlung per Rechnung wurde genehmigt.',
'checkout_mail_pay_non' => 'Deine Bestellung wurde ausgeführt.',
'checkout_mail_system_status' => 'SystemStatus:',
'my_orders' => 'Meine Bestellungen',
];