Warenwirtschaft: Review-Fix — Altdaten-PDFs zentral regenerieren (ensureFilesOnDisk)
document('combined') lief bei fehlender Alt-PDF ungeprüft in eine
PDFMerger-Exception (500); der Sammel-Druck prüfte nur den Lieferschein,
eine fehlende Rechnungs-PDF ließ den ganzen Batch platzen. Neue zentrale
InvoiceRepository::ensureFilesOnDisk() ersetzt den dreifach kopierten
Altdaten-Check (MailLogistic 2x, batchDocuments); Merge-/Response-Code im
OrderFulfillmentController zu mergedPdf()/pdfResponse() dedupliziert.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
parent
8ad0f65947
commit
f28f4b4b68
5 changed files with 242 additions and 132 deletions
|
|
@ -133,31 +133,20 @@ class OrderFulfillmentController extends Controller
|
|||
return redirect()->route('admin.inventory.orders.index');
|
||||
}
|
||||
|
||||
$merger = new PDFMerger;
|
||||
$merger->init();
|
||||
|
||||
foreach ($printable as $order) {
|
||||
if (! Invoice::isInvoice($order)) {
|
||||
(new InvoiceRepository($order))->create([]);
|
||||
} elseif (! Storage::disk('public')->exists(Invoice::getDownloadPathDelivery($order))) {
|
||||
// Datei fehlt auf der Platte (Altdaten) — mit bestehender Nummer neu erzeugen
|
||||
(new InvoiceRepository($order))->update([]);
|
||||
} else {
|
||||
// Fehlende Dateien (Altdaten) mit bestehender Nummer regenerieren —
|
||||
// prüft Rechnung UND Lieferschein (Review 20.07.2026)
|
||||
(new InvoiceRepository($order))->ensureFilesOnDisk();
|
||||
}
|
||||
|
||||
if ($data['type'] === 'combined') {
|
||||
$merger->addPDF(Invoice::getDownloadPath($order, true));
|
||||
}
|
||||
$merger->addPDF(Invoice::getDownloadPathDelivery($order, true));
|
||||
}
|
||||
|
||||
$filename = ($data['type'] === 'combined' ? 'Rechnungen-Lieferscheine-' : 'Lieferscheine-')
|
||||
.now()->format('d-m-Y_Hi').'.pdf';
|
||||
$merger->setFileName($filename);
|
||||
$merger->merge();
|
||||
|
||||
return Response::make($merger->output(), 200)
|
||||
->header('Content-Type', 'application/pdf')
|
||||
->header('Content-disposition', 'inline; filename="'.$filename.'"');
|
||||
return $this->pdfResponse($this->mergedPdf($printable, $data['type'] === 'combined', $filename), $filename);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -175,25 +164,18 @@ class OrderFulfillmentController extends Controller
|
|||
if ($type === 'label') {
|
||||
abort_unless($order->wp_label_path && Storage::disk('local')->exists($order->wp_label_path), 404);
|
||||
|
||||
return Response::make(Storage::disk('local')->get($order->wp_label_path), 200)
|
||||
->header('Content-Type', 'application/pdf')
|
||||
->header('Content-disposition', 'inline; filename="DHL-Label-Bestellung-'.$order->id.'.pdf"');
|
||||
return $this->pdfResponse(Storage::disk('local')->get($order->wp_label_path), 'DHL-Label-Bestellung-'.$order->id.'.pdf');
|
||||
}
|
||||
|
||||
abort_unless(Invoice::isInvoice($order), 404);
|
||||
// Fehlende Dateien (Altdaten) mit bestehender Nummer regenerieren — vorher
|
||||
// lief der combined-Zweig ungeprüft in eine Exception (Review 20.07.2026)
|
||||
abort_unless((new InvoiceRepository($order))->ensureFilesOnDisk(), 404);
|
||||
|
||||
if ($type === 'combined') {
|
||||
$merger = new PDFMerger;
|
||||
$merger->init();
|
||||
$merger->addPDF(Invoice::getDownloadPath($order, true));
|
||||
$merger->addPDF(Invoice::getDownloadPathDelivery($order, true));
|
||||
$filename = str_replace('Rechnung-', 'Rechnung-Lieferschein-', Invoice::getFilename($order));
|
||||
$merger->setFileName($filename);
|
||||
$merger->merge();
|
||||
|
||||
return Response::make($merger->output(), 200)
|
||||
->header('Content-Type', 'application/pdf')
|
||||
->header('Content-disposition', 'inline; filename="'.$filename.'"');
|
||||
return $this->pdfResponse($this->mergedPdf([$order], true, $filename), $filename);
|
||||
}
|
||||
|
||||
$path = $type === 'invoice' ? Invoice::getDownloadPath($order) : Invoice::getDownloadPathDelivery($order);
|
||||
|
|
@ -201,7 +183,36 @@ class OrderFulfillmentController extends Controller
|
|||
|
||||
abort_unless(Storage::disk('public')->exists($path), 404);
|
||||
|
||||
return Response::make(Storage::disk('public')->get($path), 200)
|
||||
return $this->pdfResponse(Storage::disk('public')->get($path), $filename);
|
||||
}
|
||||
|
||||
/**
|
||||
* Rechnung (optional) + Lieferschein der Bestellungen zu EINEM PDF mergen —
|
||||
* gemeinsame Implementierung für Einzel-Kombi und Sammel-Druck.
|
||||
*
|
||||
* @param iterable<ShoppingOrder> $orders
|
||||
*/
|
||||
private function mergedPdf(iterable $orders, bool $withInvoice, string $filename): string
|
||||
{
|
||||
$merger = new PDFMerger;
|
||||
$merger->init();
|
||||
|
||||
foreach ($orders as $order) {
|
||||
if ($withInvoice) {
|
||||
$merger->addPDF(Invoice::getDownloadPath($order, true));
|
||||
}
|
||||
$merger->addPDF(Invoice::getDownloadPathDelivery($order, true));
|
||||
}
|
||||
|
||||
$merger->setFileName($filename);
|
||||
$merger->merge();
|
||||
|
||||
return $merger->output();
|
||||
}
|
||||
|
||||
private function pdfResponse(string $content, string $filename)
|
||||
{
|
||||
return Response::make($content, 200)
|
||||
->header('Content-Type', 'application/pdf')
|
||||
->header('Content-disposition', 'inline; filename="'.$filename.'"');
|
||||
}
|
||||
|
|
|
|||
|
|
@ -52,10 +52,8 @@ class MailLogistic extends Mailable
|
|||
|
||||
$filename = Invoice::getFilename($this->shopping_order);
|
||||
$path = Invoice::getDownloadPath($this->shopping_order);
|
||||
if (! Storage::disk('public')->exists($path)) {
|
||||
// Datei fehlt auf der Platte (Altdaten) — mit bestehender Nummer neu erzeugen
|
||||
(new InvoiceRepository($this->shopping_order))->update([]);
|
||||
}
|
||||
// Fehlende Dateien (Altdaten) mit bestehender Nummer regenerieren — zentral
|
||||
(new InvoiceRepository($this->shopping_order))->ensureFilesOnDisk();
|
||||
if (! Storage::disk('public')->exists($path)) {
|
||||
return;
|
||||
}
|
||||
|
|
@ -75,10 +73,6 @@ class MailLogistic extends Mailable
|
|||
// Vorher hing er nur bei White-Label-Bestellungen an; Folge waren
|
||||
// Fehlversände an die Rechnungsadresse.
|
||||
$pathDelivery = Invoice::getDownloadPathDelivery($this->shopping_order);
|
||||
if (! Storage::disk('public')->exists($pathDelivery)) {
|
||||
// Datei fehlt auf der Platte (Altdaten) — mit bestehender Nummer neu erzeugen
|
||||
(new InvoiceRepository($this->shopping_order))->update([]);
|
||||
}
|
||||
if (Storage::disk('public')->exists($pathDelivery)) {
|
||||
$mail->attach(Storage::disk('public')->path($pathDelivery), [
|
||||
'as' => Invoice::getDeliveryFilename($this->shopping_order),
|
||||
|
|
|
|||
|
|
@ -2,26 +2,28 @@
|
|||
|
||||
namespace App\Repositories;
|
||||
|
||||
use PDF;
|
||||
use Storage;
|
||||
use App\Models\Attribute;
|
||||
use App\Services\Invoice;
|
||||
use App\Libraries\InvoicePDF;
|
||||
use App\Models\Attribute;
|
||||
use App\Models\ShoppingOrder;
|
||||
use App\Services\Invoice;
|
||||
use App\Services\MyPDFMerger;
|
||||
use App\Services\UserService;
|
||||
use App\Repositories\BaseRepository;
|
||||
|
||||
class InvoiceRepository extends BaseRepository {
|
||||
use PDF;
|
||||
use Storage;
|
||||
|
||||
class InvoiceRepository extends BaseRepository
|
||||
{
|
||||
private $invoice_date;
|
||||
|
||||
private $invoice_number;
|
||||
|
||||
private $filename;
|
||||
|
||||
private $dir;
|
||||
|
||||
private $delivery_dir;
|
||||
private $delivery_filename;
|
||||
|
||||
private $delivery_filename;
|
||||
|
||||
public function __construct(ShoppingOrder $model)
|
||||
{
|
||||
|
|
@ -30,10 +32,10 @@ class InvoiceRepository extends BaseRepository {
|
|||
|
||||
public function create($request = [])
|
||||
{
|
||||
//need invoice $data
|
||||
// need invoice $data
|
||||
$number = Invoice::getInvoiceNumber();
|
||||
$this->invoice_date = isset($request['invoice_date']) ? $request['invoice_date'] : $this->model->created_at->format("d.m.Y");
|
||||
$invoice_send_mail = isset($request['invoice_send_mail']) ? true: false;
|
||||
$this->invoice_date = isset($request['invoice_date']) ? $request['invoice_date'] : $this->model->created_at->format('d.m.Y');
|
||||
$invoice_send_mail = isset($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);
|
||||
|
|
@ -43,11 +45,11 @@ class InvoiceRepository extends BaseRepository {
|
|||
$this->makePDF();
|
||||
|
||||
$invoice_file = [
|
||||
'filename' => $this->filename,
|
||||
'dir' => $this->dir,
|
||||
'disk' => 'public',
|
||||
'invoice_number' => $this->invoice_number,
|
||||
'invoice_date' => $this->invoice_date,
|
||||
'filename' => $this->filename,
|
||||
'dir' => $this->dir,
|
||||
'disk' => 'public',
|
||||
'invoice_number' => $this->invoice_number,
|
||||
'invoice_date' => $this->invoice_date,
|
||||
];
|
||||
|
||||
$delivery_file = [
|
||||
|
|
@ -63,21 +65,22 @@ class InvoiceRepository extends BaseRepository {
|
|||
$this->model->save();
|
||||
Invoice::makeNextInvoiceNumber();
|
||||
|
||||
if($invoice_send_mail){
|
||||
Invoice::sendInvoiceMail($this->model);
|
||||
if ($invoice_send_mail) {
|
||||
Invoice::sendInvoiceMail($this->model);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public function update($request = []){
|
||||
if(isset($this->model->invoice['filename'])){
|
||||
public function update($request = [])
|
||||
{
|
||||
if (isset($this->model->invoice['filename'])) {
|
||||
$this->invoice_date = isset($request['invoice_date']) ? $request['invoice_date'] : Invoice::getDate($this->model);
|
||||
$invoice_send_mail = isset($request['invoice_send_mail']) ? true: false;
|
||||
$invoice_send_mail = isset($request['invoice_send_mail']) ? true : false;
|
||||
$this->invoice_number = Invoice::getNumber($this->model);
|
||||
$this->dir = Invoice::getDir($this->model);
|
||||
$this->filename = Invoice::getFilename($this->model);
|
||||
|
||||
|
||||
|
||||
$this->delivery_dir = Invoice::getDeliveryDir($this->model);
|
||||
$this->delivery_filename = Invoice::getDeliveryFilename($this->model);
|
||||
|
||||
|
|
@ -103,140 +106,173 @@ class InvoiceRepository extends BaseRepository {
|
|||
$this->model->invoice_number = $this->invoice_number;
|
||||
$this->model->save();
|
||||
|
||||
if($invoice_send_mail){
|
||||
if ($invoice_send_mail) {
|
||||
Invoice::sendInvoiceMail($this->model);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private function makePDF(){
|
||||
/**
|
||||
* Altdaten-Reparatur an EINER Stelle (Review 20.07.2026): Rechnung und/oder
|
||||
* Lieferschein fehlen auf der Platte, obwohl die DB-Metadaten existieren
|
||||
* (z. B. Storage-Umzug). Regeneriert beide Dateien mit bestehender Nummer.
|
||||
* Vorher war der Check dreifach kopiert (MailLogistic 2x, batchDocuments) und
|
||||
* prüfte im Sammel-Druck nur den Lieferschein — eine fehlende Rechnungs-PDF
|
||||
* ließ den ganzen Batch mit einer Exception platzen.
|
||||
*
|
||||
* @return bool true = beide Dateien liegen (ggf. nach Regeneration) auf der Platte
|
||||
*/
|
||||
public function ensureFilesOnDisk(): bool
|
||||
{
|
||||
if (! Invoice::isInvoice($this->model)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$invoicePath = Invoice::getDownloadPath($this->model);
|
||||
$deliveryPath = Invoice::getDownloadPathDelivery($this->model);
|
||||
|
||||
if (! Storage::disk('public')->exists($invoicePath) || ! Storage::disk('public')->exists($deliveryPath)) {
|
||||
$this->update([]);
|
||||
}
|
||||
|
||||
return Storage::disk('public')->exists($invoicePath)
|
||||
&& Storage::disk('public')->exists($deliveryPath);
|
||||
}
|
||||
|
||||
private function makePDF()
|
||||
{
|
||||
$data = [
|
||||
'shopping_order' => $this->model,
|
||||
'invoice_date' => $this->invoice_date,
|
||||
'invoice_number' => $this->invoice_number,
|
||||
];
|
||||
|
||||
if($this->model->auth_user_id){
|
||||
if ($this->model->auth_user_id) {
|
||||
UserService::checkUserTaxShippingCountry($this->model->auth_user, $this->model->country_id);
|
||||
$data = array_merge($data, UserService::getYardInfo());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
$data['delivery_items'] = $this->prepairForDelivery();
|
||||
|
||||
if(!Storage::disk('public')->exists( $this->dir )){
|
||||
Storage::disk('public')->makeDirectory($this->dir); //creates directory
|
||||
if (! Storage::disk('public')->exists($this->dir)) {
|
||||
Storage::disk('public')->makeDirectory($this->dir); // creates directory
|
||||
}
|
||||
if(!Storage::disk('public')->exists( $this->delivery_dir )){
|
||||
Storage::disk('public')->makeDirectory($this->delivery_dir); //creates directory
|
||||
if (! Storage::disk('public')->exists($this->delivery_dir)) {
|
||||
Storage::disk('public')->makeDirectory($this->delivery_dir); // creates directory
|
||||
}
|
||||
$path = Storage::disk('public')->path('');
|
||||
|
||||
$pdf_file = new InvoicePDF('pdf.invoice');
|
||||
$pdf_file->create($data, $this->filename, 'save', $path.$this->dir);
|
||||
$pdfMerger = new MyPDFMerger();
|
||||
$pdfMerger = new MyPDFMerger;
|
||||
$pdfMerger->addPDF($path.$this->dir.$this->filename);
|
||||
$file = $pdfMerger->myMerge('string', $this->filename, 'template_invoice_de');
|
||||
Storage::disk('public')->put($this->dir.$this->filename, $file);
|
||||
|
||||
$pdf_file = new InvoicePDF('pdf.delivery');
|
||||
$pdf_file->create($data, $this->delivery_filename, 'save', $path.$this->delivery_dir);
|
||||
$pdfMerger = new MyPDFMerger();
|
||||
$pdfMerger = new MyPDFMerger;
|
||||
$pdfMerger->addPDF($path.$this->delivery_dir.$this->delivery_filename);
|
||||
$file = $pdfMerger->myMerge('string', $this->delivery_filename, 'template_delivery_de');
|
||||
Storage::disk('public')->put($this->delivery_dir.$this->delivery_filename, $file);
|
||||
|
||||
|
||||
}
|
||||
//sort white label products
|
||||
private function prepairForDelivery(){
|
||||
|
||||
// sort white label products
|
||||
private function prepairForDelivery()
|
||||
{
|
||||
$shopping_order = $this->model;
|
||||
$wl_items = [];
|
||||
$items = [];
|
||||
$counter = 1;
|
||||
$count = 10000;
|
||||
foreach($shopping_order->shopping_order_items as $shopping_order_item){
|
||||
foreach ($shopping_order->shopping_order_items as $shopping_order_item) {
|
||||
$labels = [];
|
||||
$sorter = 0;
|
||||
if($shopping_order_item->product->whitelabel){
|
||||
if ($shopping_order_item->product->whitelabel) {
|
||||
|
||||
//get white label default etikett (s)
|
||||
//ist ausschlaggebend für die etikettierung, ob 1 oder mehr etiketten
|
||||
if($shopping_order_item->product->whitelabel_images){
|
||||
foreach($shopping_order_item->product->whitelabel_images as $whitelabel_image){
|
||||
// get white label default etikett (s)
|
||||
// ist ausschlaggebend für die etikettierung, ob 1 oder mehr etiketten
|
||||
if ($shopping_order_item->product->whitelabel_images) {
|
||||
foreach ($shopping_order_item->product->whitelabel_images as $whitelabel_image) {
|
||||
$labels[] = $whitelabel_image;
|
||||
}
|
||||
}
|
||||
//has white label user etikett
|
||||
if($shopping_order->auth_user){
|
||||
// has white label user etikett
|
||||
if ($shopping_order->auth_user) {
|
||||
$whitelabel_product = $shopping_order->auth_user->whitelabel_products()->where('product_id', '=', $shopping_order_item->product->id)->first();
|
||||
if($whitelabel_product && $whitelabel_product->whitelabel_images){
|
||||
foreach($whitelabel_product->whitelabel_images as $whitelabel_image){
|
||||
if($whitelabel_image->attributes && is_array($whitelabel_image->attributes)){
|
||||
foreach($labels as $key => $label){
|
||||
foreach($whitelabel_image->attributes as $attribute){
|
||||
if(in_array($attribute, $label->attributes)){
|
||||
//found and overwrite
|
||||
if(!$this->model->user_white_label){
|
||||
$this->model->user_white_label = true;
|
||||
$this->model->save();
|
||||
}
|
||||
$labels[$key] = $whitelabel_image;
|
||||
if ($whitelabel_product && $whitelabel_product->whitelabel_images) {
|
||||
foreach ($whitelabel_product->whitelabel_images as $whitelabel_image) {
|
||||
if ($whitelabel_image->attributes && is_array($whitelabel_image->attributes)) {
|
||||
foreach ($labels as $key => $label) {
|
||||
foreach ($whitelabel_image->attributes as $attribute) {
|
||||
if (in_array($attribute, $label->attributes)) {
|
||||
// found and overwrite
|
||||
if (! $this->model->user_white_label) {
|
||||
$this->model->user_white_label = true;
|
||||
$this->model->save();
|
||||
}
|
||||
$labels[$key] = $whitelabel_image;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
foreach($labels as $key=>$label){
|
||||
//label hat attribue
|
||||
$varinats = [];
|
||||
if($label->attributes && is_array($label->attributes)){
|
||||
foreach($label->attributes as $attribute){
|
||||
if($attribute = Attribute::find($attribute)){
|
||||
if(!isset($varinats[$attribute->value])){
|
||||
foreach ($labels as $key => $label) {
|
||||
// label hat attribue
|
||||
$varinats = [];
|
||||
if ($label->attributes && is_array($label->attributes)) {
|
||||
foreach ($label->attributes as $attribute) {
|
||||
if ($attribute = Attribute::find($attribute)) {
|
||||
if (! isset($varinats[$attribute->value])) {
|
||||
$temp = explode('#', $attribute->value);
|
||||
$sorter = $temp[1] > $sorter ? $temp[1] : $sorter;
|
||||
$label->varinat = [
|
||||
'key' => $attribute->value,
|
||||
'id' => $attribute->id,
|
||||
'vol' => (float) $temp[0],
|
||||
'size' => (float) $temp[1],
|
||||
'name' => $label->product_id ? 'Standard' : $attribute->name,
|
||||
];
|
||||
}
|
||||
}
|
||||
}
|
||||
}else{
|
||||
//sonst auf standard zurückgreifen
|
||||
if($shopping_order_item->product->attribute_variants){
|
||||
foreach($shopping_order_item->product->attribute_variants as $attribute_variant){
|
||||
if(!isset($varinats[$attribute_variant->attribute->value])){
|
||||
$temp = explode('#', $attribute_variant->attribute->value);
|
||||
$sorter = $temp[1] > $sorter ? $temp[1] : $sorter;
|
||||
$label->varinat = [
|
||||
'key' => $attribute_variant->attribute->value,
|
||||
'id' => $attribute_variant->attribute->id,
|
||||
'vol' => (float) $temp[0],
|
||||
'size' => (float) $temp[1],
|
||||
'name' => 'Standard',
|
||||
];
|
||||
'key' => $attribute->value,
|
||||
'id' => $attribute->id,
|
||||
'vol' => (float) $temp[0],
|
||||
'size' => (float) $temp[1],
|
||||
'name' => $label->product_id ? 'Standard' : $attribute->name,
|
||||
];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// sonst auf standard zurückgreifen
|
||||
if ($shopping_order_item->product->attribute_variants) {
|
||||
foreach ($shopping_order_item->product->attribute_variants as $attribute_variant) {
|
||||
if (! isset($varinats[$attribute_variant->attribute->value])) {
|
||||
$temp = explode('#', $attribute_variant->attribute->value);
|
||||
$sorter = $temp[1] > $sorter ? $temp[1] : $sorter;
|
||||
$label->varinat = [
|
||||
'key' => $attribute_variant->attribute->value,
|
||||
'id' => $attribute_variant->attribute->id,
|
||||
'vol' => (float) $temp[0],
|
||||
'size' => (float) $temp[1],
|
||||
'name' => 'Standard',
|
||||
];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
//sorten of white label products
|
||||
// sorten of white label products
|
||||
$sorter = intval((float) $sorter * 100 + $counter++);
|
||||
$shopping_order_item->labels = $labels;
|
||||
$wl_items[$sorter] = $shopping_order_item;
|
||||
}else{
|
||||
$wl_items[$sorter] = $shopping_order_item;
|
||||
} else {
|
||||
$items[$count++] = $shopping_order_item;
|
||||
}
|
||||
}
|
||||
//kleine zuerst
|
||||
// kleine zuerst
|
||||
ksort($wl_items);
|
||||
|
||||
return array_merge($wl_items, $items);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -278,6 +278,7 @@ Der Warenkorb kommt also **per Knopfdruck** (Fulfillment-Übergabe), nicht autom
|
|||
| 20.07.2026 | **Review-Fix: Mahnwesen-Storno über zentralen Statuswechsel** | Code-Review vor Live-Release: Der Auto-Storno der Zahlungserinnerung (`set_order_status_cancelled`) schrieb `shipped = 10` direkt und rief nur `syncSaleStock()` — **vorbei am in AP-33a extrahierten `OrderStatusService::setShipped()`**. Folge: Bestand kam zurück, aber `margin->cancellation` blieb false, `partner_commission_pending_to`/`shipped_at` blieben gesetzt und die Promotions-/Guthaben-Folgen fehlten — die **Partnerprovision einer versendeten, nie bezahlten Bestellung wäre nach Ablauf der Pending-Frist zahlbar geworden** (Rechnungs-Bestellungen werden vor Zahlungseingang versendet; die Mahn-Abfrage filtert nicht auf Versandstatus). Jetzt ruft der Mahn-Storno `OrderStatusService::setShipped($id, 10)`. | `SaleStockDeductionTest` +1: Ende-zu-Ende über `sendReminder()` — Rückbuchung + Margen-Storno + pending_to/shipped_at genullt (13 grün) |
|
||||
| 20.07.2026 | **Review-Fix: AP-32-Brücke auf Base64 umgestellt (Connector 1.5.0)** | Code-Review vor Live-Release, drei Befunde in einem Zug behoben: **(1) Die Brücke hätte in Produktion nicht funktioniert** — der Connector 1.4.0 übertrug die rohe `label_url` aus `wp-content/uploads/woocommerce_dhl_label/`, den das DHL-Plugin per „deny from all" schützt → der Laravel-Download lief immer auf 403, das Label wurde nie gespeichert, und der View-Fallback verlinkte dieselbe 403-URL. **(2) SSRF:** der Server rief eine Client-URL ohne Host-/Schema-Prüfung ab. **(3) Timeout-Mismatch:** Download bis 20 s im Request-Pfad vs. 5 s WordPress-Default → Übertragung wäre shopseitig dauerhaft „fehlgeschlagen" (Exists-400 bei jedem Retry). Jetzt: Connector 1.5.0 liest die PDF vom Shop-Dateisystem (`label_path`, realpath-Pinning auf wp-uploads, `%PDF`-Check) und sendet sie **Base64** (`wp_label_pdf`); `storeWpLabel()` dekodiert + validiert (max. 10 MB, `%PDF`), **kein `Http::get` mehr**; `wp_label_url` bleibt reines Herkunfts-Metadatum, View-Fallback-Links entfernt; Connector-Timeout 30 s. Doku `ap-32-brueckenloesung-connector-update.md` + Hinweise überarbeitet (Deployment: Label kommt erst mit Connector 1.5.0 im Shop an). | `WpLabelTransferTest` auf Base64 umgestellt (10 grün; neu: ungültiges Base64/Nicht-PDF verworfen, URL-only = Altconnector blockiert nicht) |
|
||||
| 20.07.2026 | **Review-Fix: API-Protokoll — falsch-grüne Einträge behoben** | `ApiOrderLog::recordTransfer()` löste die SKU **erneut** auf und markierte nur unbekannte Artikel als Fehler — eine Zeile mit gültiger SKU, aber **fehlender Menge/Preis** wurde von `prepareOrder()` verworfen (Position fehlt in der Bestellung!) und trotzdem grün protokolliert. Jetzt liefert `prepareOrder()` je Zeile `included`/`product_id`/`product_name` mit und das Protokoll konsumiert nur noch diese Wahrheit — `failed` = „Position nicht übernommen" (deckt SKU unbekannt UND Parameter fehlen ab); nebenbei entfallen die N+1-Produkt-Queries je Position. | `ApiOrderLogTest` +1: bekannte SKU ohne Menge ⇒ rot markiert, Position fehlt in Bestellung (9 grün) |
|
||||
| 20.07.2026 | **Review-Fix: PDF-Altdaten-Robustheit + Dedup** | Zwei 500er-Szenarien behoben: **(1)** `document('combined')` (Kombi-Druck im Board) prüfte keine Datei-Existenz — bei fehlender Alt-PDF flog eine `PDFMerger`-Exception statt 404/Regeneration. **(2)** Der Sammel-Druck regenerierte nur bei fehlendem **Lieferschein**; fehlte nur die Rechnungs-PDF, platzte der gesamte Batch (bis 100 Bestellungen). Kern: neue zentrale Methode **`InvoiceRepository::ensureFilesOnDisk()`** (prüft/regeneriert Rechnung UND Lieferschein mit bestehender Nummer) — ersetzt den dreifach kopierten Altdaten-Check (`MailLogistic` 2x, `batchDocuments`) und wird jetzt auch in `document()` genutzt. Dazu die vierfach duplizierte Merge-/Response-Sequenz im `OrderFulfillmentController` zu `mergedPdf()` + `pdfResponse()` zusammengezogen. | `OrderFulfillmentTest` +2: Kombi-Download und Sammel-Druck regenerieren fehlende Rechnungs-PDF (16 grün); `LogisticMailTest` unverändert grün |
|
||||
| _offen_ | AP-32 | Versand im System: DHL-Etiketten per API — **neu aufgenommen + Ansatz dokumentiert (§4, 15.07.2026)**; Klärungspunkte (DHL-Konto, Versandprodukte, Druck-Setup) für nächste Anforderungsrunde notiert. **Zwischenstand 17.07.2026: Brückenlösung aktiv** (Label aus dem Shop, s. Protokollzeile) — deckt den Versand-Flow ab, bis die DHL-Klärungen da sind | – |
|
||||
| _offen_ | AP-14/15/16/17 | Audit-Trail, Blockrechte, 2FA, WaWi-Einstellungen | – |
|
||||
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ use App\Models\ShoppingOrderItem;
|
|||
use App\Models\ShoppingPayment;
|
||||
use App\Models\ShoppingUser;
|
||||
use App\Models\UserAccount;
|
||||
use App\Services\Invoice;
|
||||
use App\User;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Illuminate\Support\Facades\Mail;
|
||||
|
|
@ -279,6 +280,73 @@ test('sammel-druck erzeugt fehlende dokumente und liefert ein pdf', function ()
|
|||
Mail::assertNothingSent();
|
||||
});
|
||||
|
||||
test('kombi-download regeneriert fehlende altdateien statt fehler', function () {
|
||||
// Review 20.07.2026: der combined-Zweig lief bei fehlender PDF-Datei ungeprüft
|
||||
// in eine PDFMerger-Exception (HTTP 500) — jetzt zentrale Altdaten-Regeneration
|
||||
Mail::fake();
|
||||
Storage::fake('public');
|
||||
Setting::setContentBySlug('invoice-number', 3000, 'int');
|
||||
$product = ofMakeProduct('OF Altdaten');
|
||||
$order = ofMakeOrder([['product' => $product, 'qty' => 1]]);
|
||||
$user = ofMakeUser(1);
|
||||
|
||||
$cwd = getcwd();
|
||||
chdir(public_path());
|
||||
try {
|
||||
$this->actingAs($user)
|
||||
->post(route('admin.inventory.orders.documents', $order->id))
|
||||
->assertRedirect();
|
||||
|
||||
// Altdaten-Fall: Rechnungs-PDF von der Platte gelöscht, Metadaten vorhanden
|
||||
$order->refresh();
|
||||
Storage::disk('public')->delete(Invoice::getDownloadPath($order));
|
||||
|
||||
$this->actingAs($user)
|
||||
->get(route('admin.inventory.orders.document', [$order->id, 'combined']))
|
||||
->assertOk()
|
||||
->assertHeader('Content-Type', 'application/pdf');
|
||||
} finally {
|
||||
chdir($cwd);
|
||||
}
|
||||
|
||||
Storage::disk('public')->assertExists(Invoice::getDownloadPath($order));
|
||||
});
|
||||
|
||||
test('sammel-druck regeneriert fehlende rechnungs-pdf auch bei vorhandenem lieferschein', function () {
|
||||
// Review 20.07.2026: vorher prüfte der Sammel-Druck nur den Lieferschein —
|
||||
// eine fehlende Rechnungs-PDF ließ den ganzen Batch mit 500 platzen
|
||||
Mail::fake();
|
||||
Storage::fake('public');
|
||||
Setting::setContentBySlug('invoice-number', 4000, 'int');
|
||||
$product = ofMakeProduct('OF Batch-Altdaten');
|
||||
$order = ofMakeOrder([['product' => $product, 'qty' => 1]]);
|
||||
$user = ofMakeUser(1);
|
||||
|
||||
$cwd = getcwd();
|
||||
chdir(public_path());
|
||||
try {
|
||||
$this->actingAs($user)
|
||||
->post(route('admin.inventory.orders.documents', $order->id))
|
||||
->assertRedirect();
|
||||
|
||||
$order->refresh();
|
||||
Storage::disk('public')->delete(Invoice::getDownloadPath($order));
|
||||
Storage::disk('public')->assertExists(Invoice::getDownloadPathDelivery($order));
|
||||
|
||||
$this->actingAs($user)
|
||||
->post(route('admin.inventory.orders.documents.batch'), [
|
||||
'order_ids' => [$order->id],
|
||||
'type' => 'combined',
|
||||
])
|
||||
->assertOk()
|
||||
->assertHeader('Content-Type', 'application/pdf');
|
||||
} finally {
|
||||
chdir($cwd);
|
||||
}
|
||||
|
||||
Storage::disk('public')->assertExists(Invoice::getDownloadPath($order));
|
||||
});
|
||||
|
||||
test('sammel-druck nur mit api-bestellungen wird abgelehnt', function () {
|
||||
$order = ofMakeOrder([], ['txaction' => 'extern', 'wp_invoice_path' => 'https://shop.example/invoice/2.pdf']);
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue