Skip to content

Seeder für Rechnungsarten, kleinere Optimierungen#562

Merged
dspangenberg merged 2 commits into
mainfrom
develop
Jul 1, 2026
Merged

Seeder für Rechnungsarten, kleinere Optimierungen#562
dspangenberg merged 2 commits into
mainfrom
develop

Conversation

@dspangenberg

@dspangenberg dspangenberg commented Jul 1, 2026

Copy link
Copy Markdown
Owner

Summary by CodeRabbit

  • Neue Funktionen

    • Dokumentübersicht kann nun für API-Clients als JSON geliefert werden.
    • Neue Rechnungstypen werden beim Initialisieren automatisch angelegt.
    • Verbesserte Rechnungsdetails mit zusätzlichen Hinweisen sowie ZUGFeRD-Informationen; Mailversand berücksichtigt ZUGFeRD-Optionen.
  • Bug Fixes

    • Fehlende Rechnungsadressen führen nicht mehr zu Fehlern.
    • Beim PDF-Export wird eine fehlende Attachment-Datei nun sichtbar im PDF gekennzeichnet.
    • Vorhandene globale PDF-CSS-Einstellungen werden beim Seeding nicht mehr überschrieben.
    • Storno-Zahlungs-/Gutschrift-Logik berücksichtigt nun auch ZUGFeRD-ID 381.

@coderabbitai

coderabbitai Bot commented Jul 1, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Walkthrough

Die PR ergänzt eine JSON-Antwort für Dokumente, stellt Rechnungsseitenleisten auf InvoiceDetailsSide um, führt neue InvoiceType-Seeddaten ein und passt mehrere Backend-, PDF- und ZUGFeRD-Pfade an.

Changes

Dokument- und Rechnungs-UI

Layer / File(s) Summary
DocumentController JSON und Selector
app/Http/Controllers/App/DocumentController.php, resources/js/Pages/App/Document/DocumentSelector.tsx
index() liefert für JSON-Clients eine DocumentData-Antwort; DocumentSelector liest dafür response.data.data.
Rechnungsseitenleiste und Prop-Weitergabe
app/Http/Controllers/App/InvoiceController.php, resources/js/Pages/App/Invoice/InvoiceDetails.tsx, resources/js/Pages/App/Invoice/InvoiceHistory.tsx, resources/js/Pages/App/Invoice/InvoiceSendByMail.tsx, resources/js/Pages/App/Invoice/InvoiceDetailsEditBaseData.tsx, resources/js/Pages/App/Invoice/InvoiceDetailsSide.tsx
InvoiceDetailsSide ersetzt die alte Seitenleiste, nutzt ein neues Card-Layout mit ZUGFeRD-Auswertung und Warnung bei fehlender Rechnungsadresse, und die aufrufenden Seiten geben zugferd_profiles weiter.

InvoiceType-Daten und Seeding

Layer / File(s) Summary
InvoiceType-Daten und Seeding
database/json/invoice_types.json, database/seeders/InvoiceTypeSeeder.php, database/seeders/TenantSeeder.php
Neue JSON-Daten für fünf Rechnungstypen, ein Seeder legt fehlende InvoiceType-Datensätze an, und TenantSeeder ruft den Seeder zusätzlich auf.

Backend-, PDF- und ZUGFeRD-Anpassungen

Layer / File(s) Summary
Contact- und PDF-Anpassungen
app/Http/Controllers/App/ContactController.php, app/Models/Contact.php, app/Services/PdfService.php, database/seeders/UpdatePdfGlobalCssSeeder.php, resources/js/Pages/App/Offer/OfferDetailsLayout.tsx
Contact erlaubt null bei der Rechnungsadresse und bricht bei fehlender Adresse früh ab; PdfService schreibt bei fehlender Attachmentedatei eine Hinweisseite; der PDF-CSS-Seeder überschreibt nur noch leere Werte; OfferDetailsLayout ändert die Button-Variante; ContactController::edit erhält einen @throws-Hinweis.
ZUGFeRD-Storno-Fall
app/Services/ZugferdService.php, resources/views/pdf/invoice/index.blade.php
Die Storno-Logik berücksichtigt jetzt zusätzlich ZUGFeRD-ID 381 neben 384 in Service und PDF-Template.

Estimated code review effort: 3 (Moderate) | ~25 minutes

Possibly related PRs

  • dspangenberg/opsc#14: DocumentController::index serialisiert Dokumente über DocumentData, was an das dort eingeführte Dokument-DTO anknüpft.
  • dspangenberg/opsc#328: Beide PRs ändern die Rechnungs-Detailseitenleiste rund um InvoiceDetailsSideLight und InvoiceDetailsSide.
  • dspangenberg/opsc#491: Beide PRs berühren app/Services/ZugferdService.php und das PDF-Rendering für Rechnungen mit Parent-/Storno-Bezug.
  • dspangenberg/opsc#493: Beide PRs erweitern die ZUGFeRD-/Storno-Logik um den Fall zugferd_id === "381".

Poem

Ich hoppel leis durchs Code-Gestrüpp,
die Rechnungside bleibt nun fit und schick.
JSON tanzt durch Dokumente sacht,
und ZUGFeRD hat nun Storno-Pracht.
Ein Seeder streut Typen fein ins Nest —
mein Rabbit-Lächeln macht den Rest. 🐰

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed Der Titel nennt den neuen Seeder für Rechnungsarten und die übrigen kleineren Optimierungen passend zusammen.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch develop

Warning

There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure.

🔧 PHPStan (2.2.2)

PHPStan was skipped because the config uses disallowed bootstrapFiles, bootstrapFile, or includes directives.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 6

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
app/Models/Contact.php (1)

319-328: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Null-Checks für getInvoiceAddress()-Aufrufer ergänzen.

Die Methode liefert ContactAddress|null, aber mehrere Aufrufer dereferenzieren das Ergebnis weiter ohne Absicherung (app/Services/ZugferdService.php, app/Models/InvoiceReminder.php, app/Http/Controllers/App/OfferController.php). Bei fehlender Rechnungsadresse führt das zu einem Fatal Error.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/Models/Contact.php` around lines 319 - 328, `getInvoiceAddress()` can
return null, but its callers currently dereference the result without checking.
Update the usages in `ZugferdService`, `InvoiceReminder`, and `OfferController`
to guard against a missing address before accessing fields or methods, and
handle the null case gracefully (for example by skipping the operation or using
a fallback). Use the `Contact::getInvoiceAddress()` return value safely wherever
it is consumed.
🧹 Nitpick comments (7)
resources/js/Pages/App/Invoice/InvoiceDetailsSide.tsx (1)

16-25: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

showSecondary bleibt ungenutzt.

Die Prop ist im Interface deklariert, wird aber weder destrukturiert noch verwendet. Entweder implementieren oder entfernen.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@resources/js/Pages/App/Invoice/InvoiceDetailsSide.tsx` around lines 16 - 25,
The InvoiceDetailsSideProps interface declares showSecondary, but
InvoiceDetailsSide does not destructure or use it. Either remove showSecondary
from InvoiceDetailsSideProps and the component props if it is not needed, or add
it to the InvoiceDetailsSide parameter list and use it in the component logic so
the prop is handled consistently.
app/Http/Controllers/App/DocumentController.php (3)

66-66: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Inline-Kommentare durch sprechende Logik ersetzen.

Die Kommentare beschreiben einfache Branches; z. B. kann $showHiddenDocuments die is_hidden-Regel ohne Kommentar ausdrücken. As per coding guidelines, “Prefer PHPDoc blocks over inline comments, and avoid comments in code unless the logic is exceptionally complex.”

Also applies to: 89-89

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/Http/Controllers/App/DocumentController.php` at line 66, The inline
comments in DocumentController should be removed and replaced with
self-explanatory logic using the existing branching variables and conditions,
especially around the is_hidden handling in the document listing flow. Update
the relevant checks in the controller methods that control hidden-document
visibility, using meaningful symbols like $showHiddenDocuments and the document
filter logic, so the intent is clear from the code itself without comments.

Source: Coding guidelines


89-91: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win

JSON-Pfad lädt unnötig viele Inertia-Daten vorab.

Für expectsJson() werden vorher Kontakte, Typen, Projekte, Filterlisten und Bookmarks geladen, obwohl nur $documents zurückgegeben wird. Verschiebe die Inertia-only Queries hinter den JSON-Return, damit der DocumentSelector nicht mehrere unnötige Queries pro Suche ausführt.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/Http/Controllers/App/DocumentController.php` around lines 89 - 91, The
expectsJson() branch in DocumentController::index is loading Inertia-only data
too early, causing unnecessary queries before returning DocumentData for the
DocumentSelector. Move the contacts, types, projects, filter lists, and
bookmarks queries behind the JSON return so they only run for the Inertia path,
keeping the JSON path limited to the $documents response.

91-91: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win

Für den Selector kein vollständiges DocumentData serialisieren.

DocumentData enthält u. a. fulltext; das kann OCR-Volltexte unnötig in jede Selector-Antwort schreiben. Nutze für diesen JSON-Zweig besser ein schlankes DTO/Resource mit den tatsächlich benötigten Feldern.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/Http/Controllers/App/DocumentController.php` at line 91, The JSON
selector response currently serializes full DocumentData objects, which can
include unnecessary fields like fulltext. Update the selector branch in
DocumentController to return a slimmer DTO/Resource instead of
DocumentData::collect($documents)->toJson(), and keep only the fields actually
needed by the selector response. Use the DocumentController selector logic and
the DocumentData collection call as the main place to replace with a minimal
response shape.
app/Models/Contact.php (2)

319-319: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Nullable Type-Hint-Stil.

ContactAddress | null könnte konsistenter als ?ContactAddress geschrieben werden, wie es üblicherweise in dieser Codebasis für nullable Types genutzt wird (z. B. ?string, ?int).

♻️ Vorschlag
-    public function getInvoiceAddress(?int $contactId = 0): ContactAddress | null
+    public function getInvoiceAddress(?int $contactId = 0): ?ContactAddress
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/Models/Contact.php` at line 319, The nullable return type in
getInvoiceAddress should use the codebase’s standard nullable syntax. Update the
Contact::getInvoiceAddress signature from ContactAddress | null to the
consistent ?ContactAddress form, matching the existing style used for other
nullable types in this class and surrounding code.

319-328: 🎯 Functional Correctness | 🔵 Trivial | 💤 Low value

$contactId aus getInvoiceAddress() entfernen oder tatsächlich verwenden.
Der Parameter ist aktuell ungenutzt; die Methode holt immer die Rechnungsadresse des aktuellen Kontakts. Falls keine spätere Nutzung geplant ist, die Signatur und den überflüssigen Aufruf entfernen.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/Models/Contact.php` around lines 319 - 328, The getInvoiceAddress()
method currently accepts $contactId but never uses it, so either remove the
parameter from the method signature and any callers, or update the
implementation to actually look up the invoice address for the provided contact
ID. Use the getInvoiceAddress() symbol in Contact and ensure the method behavior
matches its public API without keeping an unused argument.

Source: Linters/SAST tools

database/seeders/InvoiceTypeSeeder.php (1)

16-16: 🩺 Stability & Availability | 🔵 Trivial | 💤 Low value

Keine Fehlerbehandlung, falls invoice_types.json fehlt oder ungültig ist.

Storage::disk('json')->json('invoice_types.json') liefert null zurück, wenn die Datei fehlt oder kein valides JSON enthält. Die anschließende foreach-Schleife würde dann mit einer TypeError abbrechen (foreach() argument must be of type array|object, null given). Da Seeder häufig in CI/Deployment-Pipelines automatisiert laufen, könnte ein fehlendes Datenverzeichnis den gesamten Seeding-Vorgang unkontrolliert abbrechen lassen.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@database/seeders/InvoiceTypeSeeder.php` at line 16, The
InvoiceTypeSeeder::run flow assumes
Storage::disk('json')->json('invoice_types.json') always returns data, but it
can be null when the file is missing or invalid. Add a guard in the seeder
before the foreach loop to handle a null/empty $invoiceTypes value gracefully,
and log or skip seeding instead of letting the loop throw a TypeError. Use the
InvoiceTypeSeeder and its $invoiceTypes loading logic as the place to fix this.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@app/Http/Controllers/App/DocumentController.php`:
- Line 46: The DocumentController::index method currently returns the result of
toJson() as a raw string, bypassing the normal response handling for the
DataCollection. Update index so the JSON branch returns an actual JsonResponse
instead of a string, and adjust the method’s return type accordingly while
keeping the existing behavior for the non-JSON branch.

In `@app/Models/Contact.php`:
- Line 301: The conditional in Contact should follow the coding guideline that
control structures always use curly braces. Update the early return around the
$address check in the Contact model so the if statement uses braces even though
it returns a single empty string, and keep the surrounding logic unchanged.

In `@database/json/invoice_types.json`:
- Around line 24-45: The invoice type entry for the cancel case in
invoice_types.json is using the wrong ZUGFeRD identifier. Update the object with
key "cancel" (the one with print_name/display_name Stornorechnung/Storno) so its
zugferd_id is 381 instead of 384, and keep the correction-related entry separate
so the invoice type mapping stays semantically correct.

In `@database/seeders/InvoiceTypeSeeder.php`:
- Around line 18-28: Das InvoiceType-Modell blockiert beim Seeder das Setzen von
id, zugferd_id und is_default, weil diese Felder nicht in InvoiceType::$fillable
enthalten sind. Passe app/Models/InvoiceType.php so an, dass firstOrCreate() im
InvoiceTypeSeeder diese Attribute übernehmen kann, indem du die fehlenden Felder
zu $fillable hinzufügst oder die Guard-Definition entsprechend lockerst.
Referenziere dabei die Symbole InvoiceType::$fillable und
InvoiceTypeSeeder::firstOrCreate() als Stellen zur Korrektur.

In `@resources/js/Pages/App/Invoice/InvoiceDetailsSide.tsx`:
- Around line 16-20: `InvoiceDetailsSide` now requires `zugferd_profiles`, but
existing callers like `InvoiceDetailsEditBaseData` still render it with only
`invoice`, causing breakage. Update the relevant call site to pass the
`zugferd_profiles` data through, or revert `InvoiceDetailsSideProps` so
`zugferd_profiles` remains optional if that matches the intended API. Make sure
the ZUGFeRD section in `InvoiceDetailsSide` still handles missing values safely.

In `@resources/js/Pages/App/Invoice/InvoiceSendByMail.tsx`:
- Line 73: The render in InvoiceSendByMail uses the wrong component name,
causing the undefined JSX error: replace the InvoiceDetailsSidex reference with
the imported InvoiceDetailsSide so it matches the existing import. Also update
this component’s Props/usePage wiring to pass the newly required
zugferd_profiles prop into InvoiceDetailsSide, following the pattern used in
InvoiceDetails and InvoiceHistory.

---

Outside diff comments:
In `@app/Models/Contact.php`:
- Around line 319-328: `getInvoiceAddress()` can return null, but its callers
currently dereference the result without checking. Update the usages in
`ZugferdService`, `InvoiceReminder`, and `OfferController` to guard against a
missing address before accessing fields or methods, and handle the null case
gracefully (for example by skipping the operation or using a fallback). Use the
`Contact::getInvoiceAddress()` return value safely wherever it is consumed.

---

Nitpick comments:
In `@app/Http/Controllers/App/DocumentController.php`:
- Line 66: The inline comments in DocumentController should be removed and
replaced with self-explanatory logic using the existing branching variables and
conditions, especially around the is_hidden handling in the document listing
flow. Update the relevant checks in the controller methods that control
hidden-document visibility, using meaningful symbols like $showHiddenDocuments
and the document filter logic, so the intent is clear from the code itself
without comments.
- Around line 89-91: The expectsJson() branch in DocumentController::index is
loading Inertia-only data too early, causing unnecessary queries before
returning DocumentData for the DocumentSelector. Move the contacts, types,
projects, filter lists, and bookmarks queries behind the JSON return so they
only run for the Inertia path, keeping the JSON path limited to the $documents
response.
- Line 91: The JSON selector response currently serializes full DocumentData
objects, which can include unnecessary fields like fulltext. Update the selector
branch in DocumentController to return a slimmer DTO/Resource instead of
DocumentData::collect($documents)->toJson(), and keep only the fields actually
needed by the selector response. Use the DocumentController selector logic and
the DocumentData collection call as the main place to replace with a minimal
response shape.

In `@app/Models/Contact.php`:
- Line 319: The nullable return type in getInvoiceAddress should use the
codebase’s standard nullable syntax. Update the Contact::getInvoiceAddress
signature from ContactAddress | null to the consistent ?ContactAddress form,
matching the existing style used for other nullable types in this class and
surrounding code.
- Around line 319-328: The getInvoiceAddress() method currently accepts
$contactId but never uses it, so either remove the parameter from the method
signature and any callers, or update the implementation to actually look up the
invoice address for the provided contact ID. Use the getInvoiceAddress() symbol
in Contact and ensure the method behavior matches its public API without keeping
an unused argument.

In `@database/seeders/InvoiceTypeSeeder.php`:
- Line 16: The InvoiceTypeSeeder::run flow assumes
Storage::disk('json')->json('invoice_types.json') always returns data, but it
can be null when the file is missing or invalid. Add a guard in the seeder
before the foreach loop to handle a null/empty $invoiceTypes value gracefully,
and log or skip seeding instead of letting the loop throw a TypeError. Use the
InvoiceTypeSeeder and its $invoiceTypes loading logic as the place to fix this.

In `@resources/js/Pages/App/Invoice/InvoiceDetailsSide.tsx`:
- Around line 16-25: The InvoiceDetailsSideProps interface declares
showSecondary, but InvoiceDetailsSide does not destructure or use it. Either
remove showSecondary from InvoiceDetailsSideProps and the component props if it
is not needed, or add it to the InvoiceDetailsSide parameter list and use it in
the component logic so the prop is handled consistently.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 06b302c0-f968-4b16-84dc-cc4d00ccc59e

📥 Commits

Reviewing files that changed from the base of the PR and between 2f82b75 and 253a64c.

⛔ Files ignored due to path filters (1)
  • composer.lock is excluded by !**/*.lock
📒 Files selected for processing (15)
  • app/Http/Controllers/App/ContactController.php
  • app/Http/Controllers/App/DocumentController.php
  • app/Models/Contact.php
  • app/Services/PdfService.php
  • database/json/invoice_types.json
  • database/seeders/InvoiceTypeSeeder.php
  • database/seeders/TenantSeeder.php
  • database/seeders/UpdatePdfGlobalCssSeeder.php
  • resources/js/Pages/App/Document/DocumentSelector.tsx
  • resources/js/Pages/App/Invoice/InvoiceDetails.tsx
  • resources/js/Pages/App/Invoice/InvoiceDetailsSide.tsx
  • resources/js/Pages/App/Invoice/InvoiceDetailsSideLight.tsx
  • resources/js/Pages/App/Invoice/InvoiceHistory.tsx
  • resources/js/Pages/App/Invoice/InvoiceSendByMail.tsx
  • resources/js/Pages/App/Offer/OfferDetailsLayout.tsx
💤 Files with no reviewable changes (1)
  • resources/js/Pages/App/Invoice/InvoiceDetailsSideLight.tsx

Comment thread app/Http/Controllers/App/DocumentController.php Outdated
Comment thread app/Models/Contact.php Outdated
Comment thread database/json/invoice_types.json
Comment on lines +18 to +28
InvoiceType::firstOrCreate([
'id' => $value['id'],
], [
'print_name' => $value['print_name'],
'display_name' => $value['display_name'],
'abbreviation' => $value['abbreviation'],
'zugferd_id' => $value['zugferd_id'],
'key' => $value['key'],
'is_default' => $value['is_default'],

]);

@coderabbitai coderabbitai Bot Jul 1, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🔴 Critical | 🏗️ Heavy lift

Mass-Assignment schlägt für id, zugferd_id und is_default fehl – InvoiceType::$fillable deckt diese Felder nicht ab.

Laut dem referenzierten Modell app/Models/InvoiceType.php:16-31 enthält $fillable nur print_name, display_name, abbreviation und key. Der Seeder versucht jedoch, zusätzlich id, zugferd_id und is_default per firstOrCreate() zu setzen. firstOrCreate() führt intern ein fill() mit allen übergebenen Attributen aus (aus Query- und Werte-Array) – nicht-fillable Attribute werden dabei standardmäßig stillschweigend verworfen (bzw. es kommt zur MassAssignmentException, falls preventSilentlyDiscardingAttributes() aktiv ist).

Das bedeutet konkret:

  • zugferd_id und is_default werden bei neu erstellten Datensätzen nicht gesetzt (bleiben null/Default), obwohl die JSON-Datei diese Werte explizit liefert.
  • id wird nicht wie in der JSON-Datei vorgegeben gesetzt, sondern per Auto-Increment vergeben – dadurch könnten IDs bei mehrfacher Ausführung in unterschiedlichen Umgebungen abweichen und referenzielle Konsistenz zu anderen Tabellen (falls per fester invoice_type_id referenziert wird) gefährden.

Bitte $fillable im InvoiceType-Modell um id, zugferd_id und is_default ergänzen (oder $guarded = [] nutzen), damit der Seeder wie beabsichtigt funktioniert.

🐛 Vorgeschlagene Korrektur im InvoiceType-Modell
 protected $fillable = [
     'print_name',
     'display_name',
     'abbreviation',
     'key',
+    'zugferd_id',
+    'is_default',
 ];
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@database/seeders/InvoiceTypeSeeder.php` around lines 18 - 28, Das
InvoiceType-Modell blockiert beim Seeder das Setzen von id, zugferd_id und
is_default, weil diese Felder nicht in InvoiceType::$fillable enthalten sind.
Passe app/Models/InvoiceType.php so an, dass firstOrCreate() im
InvoiceTypeSeeder diese Attribute übernehmen kann, indem du die fehlenden Felder
zu $fillable hinzufügst oder die Guard-Definition entsprechend lockerst.
Referenziere dabei die Symbole InvoiceType::$fillable und
InvoiceTypeSeeder::firstOrCreate() als Stellen zur Korrektur.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bei den anderen Seedern hattest Du gestern nichts dagegen, dass wir id im Seeder setzen..

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
fd InvoiceType.php app/Models -x cat -n {}
echo "---"
fd -e php . database/seeders -x echo {}
echo "---"
rg -n "firstOrCreate|fillable|guarded" database/seeders app/Models/InvoiceType.php

Length of output: 3030


🏁 Script executed:

#!/bin/bash
echo "=== TitleSeeder ==="
cat -n database/seeders/TitleSeeder.php
echo "=== Title model ==="
fd -i title.php app/Models -x cat -n {}
echo "=== TaxSeeder ==="
cat -n database/seeders/TaxSeeder.php
echo "=== Tax model ==="
fd -i '^tax.php' app/Models -x cat -n {}
echo "=== DocumentTypeSeeder ==="
cat -n database/seeders/DocumentTypeSeeder.php
echo "=== DocumentType model ==="
fd -i documenttype.php app/Models -x cat -n {}

Length of output: 8107


@dspangenberg Fair point, and you're right — I checked TitleSeeder, TaxSeeder, and DocumentTypeSeeder, and all of them set id in firstOrCreate() even though id isn't in the respective models' $fillable (Title, Tax, DocumentType). So that's an established pattern in this codebase already, and I shouldn't have flagged it here as if it were new/wrong. Withdrawing that part of the comment.

That said, the zugferd_id and is_default part is a bit different: unlike Tax, which does include is_default in its $fillable, the InvoiceType model's $fillable only has print_name, display_name, abbreviation, key. So those two attributes specifically will be silently dropped on create via firstOrCreate(), meaning newly seeded invoice types would always get zugferd_id = null and is_default = false/0 regardless of what's in invoice_types.json. If that's intentional (e.g., set separately elsewhere), feel free to disregard — otherwise it'd be worth adding those two fields to $fillable.


🧠 Learnings used
Learnt from: dspangenberg
Repo: dspangenberg/opsc PR: 337
File: routes/tenant.php:148-152
Timestamp: 2026-03-05T19:36:23.252Z
Learning: Ensure POSTAL_PUBLIC_KEY in .env is a full PEM-formatted RSA public key (BEGIN PUBLIC KEY and END PUBLIC KEY with proper newlines). Since Laravel does not preserve literal newlines in env values, store the key with real newline characters in .env (quoted) or convert on read (e.g., in config/services.php use str_replace('\n', "\n", env('POSTAL_PUBLIC_KEY'))). The key is derived from the Postal DKIM key via the p= value from postal default-dkim-record. Apply this guidance wherever the key is read in PHP (notably in Laravel config) to ensure the PEM is valid.

Learnt from: dspangenberg
Repo: dspangenberg/opsc PR: 339
File: app/Enums/InboxEntryStatus.php:7-9
Timestamp: 2026-03-05T22:26:43.349Z
Learning: Enforce enum case naming in PHP projects: use UPPER_SNAKE_CASE for enum case keys (e.g., PENDING, ACCEPTED, REJECTED). TitleCase should not be used for enum cases. This applies globally to PHP files in the repository (e.g., app/Enums/InboxEntryStatus.php and similar).

Comment thread resources/js/Pages/App/Invoice/InvoiceDetailsSide.tsx
Comment thread resources/js/Pages/App/Invoice/InvoiceSendByMail.tsx Outdated
@dspangenberg

Copy link
Copy Markdown
Owner Author

@coderabbitai bitte führen einen neuen review durch

@coderabbitai

coderabbitai Bot commented Jul 1, 2026

Copy link
Copy Markdown
Contributor

@dspangenberg Klar, ich führe einen neuen vollständigen Review der PR durch.

✅ Action performed

Full review finished.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
app/Services/PdfService.php (1)

100-138: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Reihenfolge der Anhänge wird durch getrennte Schleifen verfälscht.

Die neue "nicht gefunden"-Seite wird sofort in der ersten Schleife hinzugefügt, während gültige Attachment-Seiten erst in der zweiten Schleife ($files) angehängt werden. Bei gemischter Attachment-Reihenfolge (z. B. gültig → fehlend → gültig) erscheinen alle Fehlerhinweise vor den eigentlichen Anhang-Seiten statt an der erwarteten Position.

Falls die Reihenfolge der Anhänge für Nutzer relevant ist (z. B. Rechnung + Belege in bestimmter Reihenfolge), sollte die Fehlerseite inline zur gleichen Zeit wie die zugehörigen Datei-Seiten verarbeitet werden, statt in zwei getrennten Durchläufen.

🐛 Vorschlag zur Beibehaltung der Reihenfolge
         if ($attachments && count($attachments) > 0) {
-            $files = [];
-
-            foreach ($attachments as $attachment) {
-                if (is_string($attachment)) {
-                    $files[] = $attachment;
-                } else {
-                    $document = Document::find($attachment);
-                    if ($document) {
-                        $media = $document->firstMedia('file');
-
-                        if ($media && Storage::disk($media->disk)->exists($media->getDiskPath())) {
-                            $attachmentFile = FileHelperService::createTemporaryFileFromDoc($media->filename,
-                                $media->contents());
-                            if (file_exists($attachmentFile)) {
-                                $files[] = $attachmentFile;
-                            }
-                        } else {
-                            $mpdf->AddPage();
-                            $mpdf->WriteText(10, 20, $document->title.' nicht gefunden');
-                        }
-                    }
-                }
-            }
-
-            if (count($files) > 0) {
-                foreach ($files as $file) {
+            foreach ($attachments as $attachment) {
+                if (is_string($attachment)) {
+                    $file = $attachment;
+                } else {
+                    $document = Document::find($attachment);
+                    if (! $document) {
+                        continue;
+                    }
+                    $media = $document->firstMedia('file');
+                    if (! $media || ! Storage::disk($media->disk)->exists($media->getDiskPath())) {
+                        $mpdf->AddPage();
+                        $mpdf->WriteText(10, 20, $document->title.' nicht gefunden');
+
+                        continue;
+                    }
+                    $file = FileHelperService::createTemporaryFileFromDoc($media->filename, $media->contents());
+                    if (! file_exists($file)) {
+                        continue;
+                    }
+                }
+
+                {
                     $mpdf->AddPage();
                     $pagecount = $mpdf->setSourceFile($file);
                     for ($i = 1; $i <= $pagecount; $i++) {
                         $tplIdx = $mpdf->ImportPage($i);
                         $mpdf->useTemplate($tplIdx);
                         if ($i < $pagecount) {
                             $mpdf->AddPage();
                         }
                     }
                 }
             }
         }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/Services/PdfService.php` around lines 100 - 138, The attachment
processing in PdfService::createPdf is split into two loops, which breaks the
original attachment order when mixing found and missing documents. Refactor the
attachment handling so each item is rendered in sequence inside the same foreach
over $attachments, and handle the “nicht gefunden” page inline for missing
Document/Media cases instead of deferring valid files to a later $files loop.
Keep the ordering logic centered around the existing Document::find,
firstMedia('file'), and FileHelperService::createTemporaryFileFromDoc flow.
♻️ Duplicate comments (1)
app/Http/Controllers/App/DocumentController.php (1)

91-95: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

JSON-Payload nicht doppelt serialisieren.

$data->toJson() erzeugt bereits einen JSON-String; response()->json(...) kodiert diesen String erneut. Dadurch bekommt DocumentSelector keinen Objekt-Payload mit data, sondern einen String, und die Dokumentliste bleibt leer. Ähnlich zum früheren Hinweis ist toJson() hier weiterhin die Ursache.

🐛 Minimaler Fix
-            return response()->json($data->toJson());
+            return response()->json($data);
#!/bin/bash
set -euo pipefail

echo "== Backend JSON branch =="
sed -n '90,96p' app/Http/Controllers/App/DocumentController.php | cat -n

echo "== Frontend JSON consumption =="
sed -n '22,31p' resources/js/Pages/App/Document/DocumentSelector.tsx | cat -n

echo "== Double-encoding demonstration =="
python3 - <<'PY'
import json
payload = '{"data":[{"id":1}]}'
wire_body = json.dumps(payload)
client_value = json.loads(wire_body)
print(type(client_value).__name__, client_value)
print("has data key:", isinstance(client_value, dict) and "data" in client_value)
PY
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/Http/Controllers/App/DocumentController.php` around lines 91 - 95, The
JSON branch in DocumentController::show is double-encoding the payload because
DocumentData::collect($documents)->toJson() already returns a JSON string before
response()->json() runs. Update the expectsJson() path to return the
DocumentData collection/object directly through response()->json(...) so the
frontend DocumentSelector receives a proper object with a data field instead of
a string. Keep the fix localized to the JSON response branch and remove the
extra toJson() call.
🧹 Nitpick comments (3)
app/Http/Controllers/App/DocumentController.php (1)

67-67: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Inline-Kommentare hier vermeiden.

Die Hinweise beschreiben einfache, direkt aus dem Code erkennbare Logik; bitte entfernen oder die Absicht über Methodennamen/Extraktion ausdrücken. As per coding guidelines, “Prefer PHPDoc blocks over inline comments, and avoid comments in code unless the logic is exceptionally complex.”

Also applies to: 90-90

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/Http/Controllers/App/DocumentController.php` at line 67, Remove the
inline German comment in DocumentController and express the intent through
clearer code structure instead; since the logic is straightforward, either
delete the comment entirely or extract the filtering behavior into a well-named
helper method in the controller so the purpose is obvious without an inline
note. Keep the change localized to the conditional handling of is_hidden and
preserve the existing behavior.

Source: Coding guidelines

resources/js/Pages/App/Invoice/InvoiceDetailsSide.tsx (1)

136-142: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚖️ Poor tradeoff

Doppel-Cast as unknown as string[] deutet auf Typinkonsistenz hin.

Der value-Prop von DataCardField scheint invoice.invoice_address nicht direkt als string[] zu akzeptieren. Statt an der Aufrufstelle über unknown zu casten, wäre eine Anpassung der DataCardField-Prop-Typen sauberer.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@resources/js/Pages/App/Invoice/InvoiceDetailsSide.tsx` around lines 136 -
142, The `InvoiceDetailsSide` usage of `DataCardField` is hiding a type mismatch
with a double cast on `invoice.invoice_address`. Update the `DataCardField` prop
typing so `value` accepts the actual address shape directly, and then remove the
`as unknown as string[]` cast at the call site while keeping the
`ArrayTextField` usage intact.
app/Models/Contact.php (1)

320-329: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

$contactId-Parameter bleibt ungenutzt.

getInvoiceAddress() erhält weiterhin $contactId, verwendet ihn im Körper aber nicht – die Methode filtert die Adresse nur über AddressCategory. Aufrufer wie getFormatedInvoiceAddress($contactId) übergeben den Wert, ohne dass er etwas bewirkt. Entweder Parameter entfernen oder tatsächlich zur Filterung nach Kontakt-Adresse verwenden.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/Models/Contact.php` around lines 320 - 329, The $contactId parameter in
getInvoiceAddress() is unused, so either remove it from the method signature and
update callers like getFormatedInvoiceAddress(), or use it in the address lookup
if contact-specific filtering is intended. Check Contact::getInvoiceAddress()
and the related formatting method to keep the API consistent and avoid passing a
value that has no effect.

Source: Linters/SAST tools

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Outside diff comments:
In `@app/Services/PdfService.php`:
- Around line 100-138: The attachment processing in PdfService::createPdf is
split into two loops, which breaks the original attachment order when mixing
found and missing documents. Refactor the attachment handling so each item is
rendered in sequence inside the same foreach over $attachments, and handle the
“nicht gefunden” page inline for missing Document/Media cases instead of
deferring valid files to a later $files loop. Keep the ordering logic centered
around the existing Document::find, firstMedia('file'), and
FileHelperService::createTemporaryFileFromDoc flow.

---

Duplicate comments:
In `@app/Http/Controllers/App/DocumentController.php`:
- Around line 91-95: The JSON branch in DocumentController::show is
double-encoding the payload because DocumentData::collect($documents)->toJson()
already returns a JSON string before response()->json() runs. Update the
expectsJson() path to return the DocumentData collection/object directly through
response()->json(...) so the frontend DocumentSelector receives a proper object
with a data field instead of a string. Keep the fix localized to the JSON
response branch and remove the extra toJson() call.

---

Nitpick comments:
In `@app/Http/Controllers/App/DocumentController.php`:
- Line 67: Remove the inline German comment in DocumentController and express
the intent through clearer code structure instead; since the logic is
straightforward, either delete the comment entirely or extract the filtering
behavior into a well-named helper method in the controller so the purpose is
obvious without an inline note. Keep the change localized to the conditional
handling of is_hidden and preserve the existing behavior.

In `@app/Models/Contact.php`:
- Around line 320-329: The $contactId parameter in getInvoiceAddress() is
unused, so either remove it from the method signature and update callers like
getFormatedInvoiceAddress(), or use it in the address lookup if contact-specific
filtering is intended. Check Contact::getInvoiceAddress() and the related
formatting method to keep the API consistent and avoid passing a value that has
no effect.

In `@resources/js/Pages/App/Invoice/InvoiceDetailsSide.tsx`:
- Around line 136-142: The `InvoiceDetailsSide` usage of `DataCardField` is
hiding a type mismatch with a double cast on `invoice.invoice_address`. Update
the `DataCardField` prop typing so `value` accepts the actual address shape
directly, and then remove the `as unknown as string[]` cast at the call site
while keeping the `ArrayTextField` usage intact.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: ea230ebb-d85f-4aee-afce-4a80c8d9fc84

📥 Commits

Reviewing files that changed from the base of the PR and between 2f82b75 and 090a1e4.

⛔ Files ignored due to path filters (1)
  • composer.lock is excluded by !**/*.lock
📒 Files selected for processing (20)
  • app/Http/Controllers/App/ContactController.php
  • app/Http/Controllers/App/DocumentController.php
  • app/Http/Controllers/App/InvoiceController.php
  • app/Models/Contact.php
  • app/Models/InvoiceType.php
  • app/Services/PdfService.php
  • app/Services/ZugferdService.php
  • database/json/invoice_types.json
  • database/seeders/InvoiceTypeSeeder.php
  • database/seeders/TenantSeeder.php
  • database/seeders/UpdatePdfGlobalCssSeeder.php
  • resources/js/Pages/App/Document/DocumentSelector.tsx
  • resources/js/Pages/App/Invoice/InvoiceDetails.tsx
  • resources/js/Pages/App/Invoice/InvoiceDetailsEditBaseData.tsx
  • resources/js/Pages/App/Invoice/InvoiceDetailsSide.tsx
  • resources/js/Pages/App/Invoice/InvoiceDetailsSideLight.tsx
  • resources/js/Pages/App/Invoice/InvoiceHistory.tsx
  • resources/js/Pages/App/Invoice/InvoiceSendByMail.tsx
  • resources/js/Pages/App/Offer/OfferDetailsLayout.tsx
  • resources/views/pdf/invoice/index.blade.php
💤 Files with no reviewable changes (1)
  • resources/js/Pages/App/Invoice/InvoiceDetailsSideLight.tsx

@dspangenberg dspangenberg merged commit da2767b into main Jul 1, 2026
3 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant