model = $model; } public function create($request = []) { // Wrap entire invoice creation in transaction to ensure atomicity return \DB::transaction(function () use ($request) { // Get and increment invoice number atomically (includes its own lock) $number = Invoice::makeNextInvoiceNumber(); if ($payt = $this->model->getLastShoppingPaymentTransaction()) { $invoice_date = $payt->created_at->format('d.m.Y'); } $this->invoice_date = isset($request['invoice_date']) ? $request['invoice_date'] : $invoice_date; $invoice_send_mail = isset($request['invoice_send_mail']) && $request['invoice_send_mail'] ? true : false; $this->invoice_number = Invoice::createInvoiceNumber($number, $this->invoice_date); $this->dir = Invoice::getInvoiceStorageDir($this->invoice_date); $this->filename = Invoice::makeInvoiceFilename($this->invoice_number); $this->delivery_dir = Invoice::getDeliveryStorageDir($this->invoice_date); $this->delivery_filename = Invoice::makeDeliveryFilename($this->invoice_number); $this->makePDF(); $user_invoice = UserInvoice::create([ 'shopping_order_id' => $this->model->id, 'year' => \Carbon::parse($this->invoice_date)->format('Y'), 'month' => \Carbon::parse($this->invoice_date)->format('m'), 'date' => $this->invoice_date, 'full_number' => $this->invoice_number, 'number' => $number, 'filename' => $this->filename, 'dir' => $this->dir, 'delivery_filename' => $this->delivery_filename, 'delivery_dir' => $this->delivery_dir, 'disk' => 'public', 'status' => $this->model->getStatusByOrder(), ]); if ($invoice_send_mail) { Invoice::sendInvoiceMail($this->model, $user_invoice); } return $user_invoice; }); } public function update($request = []) { if ($user_invoice = $this->model->user_invoice) { $number = $user_invoice->number; $this->invoice_date = isset($request['invoice_date']) ? $request['invoice_date'] : $user_invoice->date; $invoice_send_mail = isset($request['invoice_send_mail']) ? false : true; $this->invoice_number = Invoice::createInvoiceNumber($number, $this->invoice_date); $this->dir = Invoice::getInvoiceStorageDir($this->invoice_date); $this->filename = Invoice::makeInvoiceFilename($this->invoice_number); $this->delivery_dir = Invoice::getDeliveryStorageDir($this->invoice_date); $this->delivery_filename = Invoice::makeDeliveryFilename($this->invoice_number); $this->user_sales_volume = UserSalesVolume::where('user_invoice_id', $this->model->user_invoice->id)->first(); $this->makePDF(); $user_invoice->fill([ 'shopping_order_id' => $this->model->id, 'year' => \Carbon::parse($this->invoice_date)->format('Y'), 'month' => \Carbon::parse($this->invoice_date)->format('m'), 'date' => $this->invoice_date, 'full_number' => $this->invoice_number, 'number' => $number, 'filename' => $this->filename, 'dir' => $this->dir, 'delivery_filename' => $this->delivery_filename, 'delivery_dir' => $this->delivery_dir, 'disk' => 'public', ])->save(); if ($invoice_send_mail) { Invoice::sendInvoiceMail($this->model, $user_invoice); } return $user_invoice; } return null; } /** * Erstellt die PDFs für Rechnung und Lieferschein. * Das deutsche Original wird immer erstellt (Finanzamt-Anforderung). * Bei anderer Kundensprache wird zusätzlich eine Kopie in der Kundensprache erstellt. */ private function makePDF() { $data = [ 'shopping_order' => $this->model, 'invoice_date' => $this->invoice_date, 'invoice_number' => $this->invoice_number, 'user_sales_volume' => $this->user_sales_volume, ]; 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->dir)) { Storage::disk('public')->makeDirectory($this->dir); } if (! Storage::disk('public')->exists($this->delivery_dir)) { Storage::disk('public')->makeDirectory($this->delivery_dir); } // Kundensprache ermitteln $customerLocale = $this->model->shopping_user ? $this->model->shopping_user->getLocale() : 'de'; $originalLocale = \App::getLocale(); // 1. IMMER deutsches Original erstellen (Finanzamt-Anforderung) \App::setLocale('de'); $this->createPDFFiles($data, 'de'); // 2. Wenn Kundensprache != DE, Kopie in Kundensprache erstellen if ($customerLocale && $customerLocale !== 'de') { \App::setLocale($customerLocale); $this->createPDFFiles($data, $customerLocale); } // Locale zurücksetzen \App::setLocale($originalLocale); } /** * Erstellt die PDF-Dateien für eine bestimmte Sprache. */ private function createPDFFiles(array $data, string $locale) { $path = Storage::disk('public')->path(''); // Dateinamen für diese Sprache $invoiceFilename = Invoice::makeInvoiceFilenameLocale($this->invoice_number, $locale); $deliveryFilename = Invoice::makeDeliveryFilenameLocale($this->invoice_number, $locale); // Kopie-Flag: true wenn nicht Deutsch (das Original) $data['is_copy'] = ($locale !== 'de'); // Template basierend auf Locale $template = $this->getTemplateForLocale($locale); // Rechnung erstellen $pdf_file = new InvoicePDF('pdf.invoice'); $pdf_file->create($data, $invoiceFilename, 'save', $path.$this->dir); $pdfMerger = new MyPDFMerger; $pdfMerger->addPDF($path.$this->dir.$invoiceFilename); $file = $pdfMerger->myMerge('string', $invoiceFilename, $template); Storage::disk('public')->put($this->dir.$invoiceFilename, $file); // Lieferschein erstellen (außer bei Sammelbestellung) if (! $this->model->shopping_collect_order) { $pdf_file = new InvoicePDF('pdf.delivery'); $pdf_file->create($data, $deliveryFilename, 'save', $path.$this->delivery_dir); $pdfMerger = new MyPDFMerger; $pdfMerger->addPDF($path.$this->delivery_dir.$deliveryFilename); $file = $pdfMerger->myMerge('string', $deliveryFilename, $template); Storage::disk('public')->put($this->delivery_dir.$deliveryFilename, $file); } } /** * Gibt das PDF-Template für die angegebene Locale zurück. * Verfügbare Templates werden aus config/localization.php geladen. */ private function getTemplateForLocale(string $locale): string { $availableTemplates = config('localization.availableTemplates', ['de']); if (in_array($locale, $availableTemplates)) { return 'template_invoice_'.$locale; } return 'template_invoice_de'; } public function userSalesVolume() {} public function createAndSalesVolume($request = []) { $this->user_sales_volume = SalesPointsVolume::User($this->model); if (! Util::isTestSystem(true)) { // rechnung erstellen nur in production $user_invoice = $this->create($request); $this->user_sales_volume->user_invoice_id = $user_invoice->id; $this->user_sales_volume->save(); } // Incentive: Track sales volume points IncentiveTracker::trackSalesVolume($this->user_sales_volume); } /** * Erstellt eine Stornorechnung mit Punktekorrektur * * @param array $request * @return UserInvoice */ public function createCancellation($request = []) { return \DB::transaction(function () use ($request) { $original_invoice = $this->model->user_invoice; if (! $original_invoice) { throw new \Exception('Keine Originalrechnung gefunden.'); } // Nächste Rechnungsnummer für Storno holen $number = Invoice::makeNextInvoiceNumber(); // Stornodatum $cancellation_date = isset($request['cancellation_date']) ? $request['cancellation_date'] : now()->format('d.m.Y'); $cancellation_send_mail = isset($request['cancellation_send_mail']) && $request['cancellation_send_mail'] ? true : false; // Rechnungsnummer erstellen $cancellation_number = Invoice::createInvoiceNumber($number, $cancellation_date); $cancellation_dir = Invoice::getInvoiceStorageDir($cancellation_date); $cancellation_filename = Invoice::makeCancellationFilename($cancellation_number); $cancellation_delivery_dir = Invoice::getDeliveryStorageDir($cancellation_date); $cancellation_delivery_filename = Invoice::makeCancellationDeliveryFilename($cancellation_number); // Stornorechnung PDF erstellen $this->makeCancellationPDF( $cancellation_date, $cancellation_number, $cancellation_dir, $cancellation_filename, $cancellation_delivery_dir, $cancellation_delivery_filename, $original_invoice ); // Stornorechnung in DB speichern $cancellation_invoice = UserInvoice::create([ 'shopping_order_id' => $this->model->id, 'year' => \Carbon::parse($cancellation_date)->format('Y'), 'month' => \Carbon::parse($cancellation_date)->format('m'), 'date' => $cancellation_date, 'full_number' => $cancellation_number, 'number' => $number, 'filename' => $cancellation_filename, 'dir' => $cancellation_dir, 'delivery_filename' => $cancellation_delivery_filename, 'delivery_dir' => $cancellation_delivery_dir, 'disk' => 'public', 'cancellation' => true, 'status' => $original_invoice->status === 1 ? 11 : 12, // 11 = storniert B., 12 = storniert Shop ]); // Original-Rechnung als storniert markieren $original_invoice->cancellation = true; $original_invoice->cancellation_id = $cancellation_invoice->id; $original_invoice->cancellation_date = $cancellation_date; $original_invoice->save(); // Bestellstatus auf "storniert" setzen $this->model->txaction = 'cancelled'; // Versandstatus auf "storniert" (10) setzen, wenn noch nicht versendet if (in_array($this->model->shipped, [0, 1])) { $this->model->shipped = 10; } $this->model->save(); \Log::info('Bestellstatus aktualisiert nach Storno', [ 'order_id' => $this->model->id, 'txaction' => $this->model->txaction, 'shipped' => $this->model->shipped, ]); // Punktekorrektur durchführen (nach Erstellung der Stornorechnung) $this->correctPointsForCancellation($original_invoice, $cancellation_invoice); // Optional: E-Mail versenden if ($cancellation_send_mail) { Invoice::sendInvoiceMail($this->model, $cancellation_invoice); } return $cancellation_invoice; }); } /** * Erstellt die Storno-PDFs (Rechnung und Lieferschein) */ private function makeCancellationPDF( $cancellation_date, $cancellation_number, $cancellation_dir, $cancellation_filename, $cancellation_delivery_dir, $cancellation_delivery_filename, $original_invoice ) { $data = [ 'shopping_order' => $this->model, 'invoice_date' => $cancellation_date, 'invoice_number' => $cancellation_number, 'original_invoice' => $original_invoice, 'is_cancellation' => true, ]; if ($this->model->auth_user_id) { UserService::checkUserTaxShippingCountry($this->model->auth_user, $this->model->country_id); $data = array_merge($data, UserService::getYardInfo()); } // Verzeichnisse erstellen if (! Storage::disk('public')->exists($cancellation_dir)) { Storage::disk('public')->makeDirectory($cancellation_dir); } if (! Storage::disk('public')->exists($cancellation_delivery_dir)) { Storage::disk('public')->makeDirectory($cancellation_delivery_dir); } // Kundensprache ermitteln $customerLocale = $this->model->shopping_user ? $this->model->shopping_user->getLocale() : 'de'; $originalLocale = \App::getLocale(); // Deutsches Original (Finanzamt-Anforderung) \App::setLocale('de'); $this->createCancellationPDFFiles( $data, 'de', $cancellation_number, $cancellation_dir, $cancellation_filename, $cancellation_delivery_dir, $cancellation_delivery_filename ); // Lokalisierte Version wenn gewünscht if ($customerLocale && $customerLocale !== 'de') { \App::setLocale($customerLocale); $data['is_copy'] = true; $localizedFilename = str_replace('.pdf', '-'.$customerLocale.'.pdf', $cancellation_filename); $localizedDeliveryFilename = str_replace('.pdf', '-'.$customerLocale.'.pdf', $cancellation_delivery_filename); $this->createCancellationPDFFiles( $data, $customerLocale, $cancellation_number, $cancellation_dir, $localizedFilename, $cancellation_delivery_dir, $localizedDeliveryFilename ); } \App::setLocale($originalLocale); } /** * Erstellt die PDF-Dateien für eine Stornorechnung in einer bestimmten Sprache */ private function createCancellationPDFFiles( array $data, string $locale, string $cancellation_number, string $cancellation_dir, string $cancellation_filename, string $cancellation_delivery_dir, string $cancellation_delivery_filename ) { $path = Storage::disk('public')->path(''); $template = $this->getTemplateForLocale($locale); // Stornorechnung erstellen $pdf_file = new InvoicePDF('pdf.cancellation'); $pdf_file->create($data, $cancellation_filename, 'save', $path.$cancellation_dir); $pdfMerger = new MyPDFMerger; $pdfMerger->addPDF($path.$cancellation_dir.$cancellation_filename); $file = $pdfMerger->myMerge('string', $cancellation_filename, $template); Storage::disk('public')->put($cancellation_dir.$cancellation_filename, $file); // Storno-Lieferschein erstellen (außer bei Sammelbestellung) if (! $this->model->shopping_collect_order) { $pdf_file = new InvoicePDF('pdf.cancellation_delivery'); $pdf_file->create($data, $cancellation_delivery_filename, 'save', $path.$cancellation_delivery_dir); $pdfMerger = new MyPDFMerger; $pdfMerger->addPDF($path.$cancellation_delivery_dir.$cancellation_delivery_filename); $file = $pdfMerger->myMerge('string', $cancellation_delivery_filename, $template); Storage::disk('public')->put($cancellation_delivery_dir.$cancellation_delivery_filename, $file); } } /** * Korrigiert die Punkte nach Stornierung einer Rechnung * Nutzt den SalesPointsVolume Service für konsistente Berechnung * * @param UserInvoice $original_invoice Die ursprüngliche Rechnung * @param UserInvoice $cancellation_invoice Die Stornorechnung */ private function correctPointsForCancellation($original_invoice, $cancellation_invoice) { // Original UserSalesVolume finden $original_sales_volume = UserSalesVolume::where('user_invoice_id', $original_invoice->id)->first(); if (! $original_sales_volume) { \Log::warning('Keine UserSalesVolume gefunden für Rechnung', [ 'invoice_id' => $original_invoice->id, 'order_id' => $this->model->id, ]); return; } // Service-Methode verwenden für konsistente Punktekorrektur SalesPointsVolume::cancelSalesPointsVolume($original_sales_volume, $cancellation_invoice->id); } }